@silverassist/agents-toolkit 2.0.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/LICENSE +135 -0
- package/README.md +388 -0
- package/bin/cli.js +940 -0
- package/package.json +52 -0
- package/src/index.js +58 -0
- package/templates/agents/AGENTS.codex.md +195 -0
- package/templates/agents/AGENTS.md +195 -0
- package/templates/agents/CLAUDE.md +213 -0
- package/templates/agents/copilot-instructions.md +83 -0
- package/templates/shared/instructions/css-styling.instructions.md +142 -0
- package/templates/shared/instructions/documentation-language.instructions.md +216 -0
- package/templates/shared/instructions/github-workflow.instructions.md +245 -0
- package/templates/shared/instructions/php-standards.instructions.md +293 -0
- package/templates/shared/instructions/react-components.instructions.md +196 -0
- package/templates/shared/instructions/server-actions.instructions.md +429 -0
- package/templates/shared/instructions/testing-standards.instructions.md +264 -0
- package/templates/shared/instructions/tests.instructions.md +103 -0
- package/templates/shared/instructions/typescript.instructions.md +106 -0
- package/templates/shared/instructions/wordpress-plugin-architecture.instructions.md +238 -0
- package/templates/shared/prompts/README.md +129 -0
- package/templates/shared/prompts/_partials/README.md +57 -0
- package/templates/shared/prompts/_partials/documentation.md +203 -0
- package/templates/shared/prompts/_partials/git-operations.md +171 -0
- package/templates/shared/prompts/_partials/github-integration.md +100 -0
- package/templates/shared/prompts/_partials/jira-integration.md +155 -0
- package/templates/shared/prompts/_partials/pr-template.md +216 -0
- package/templates/shared/prompts/_partials/validations.md +87 -0
- package/templates/shared/prompts/add-tests.prompt.md +151 -0
- package/templates/shared/prompts/analyze-github-issue.prompt.md +73 -0
- package/templates/shared/prompts/analyze-ticket.prompt.md +63 -0
- package/templates/shared/prompts/create-plan.prompt.md +118 -0
- package/templates/shared/prompts/create-pr.prompt.md +139 -0
- package/templates/shared/prompts/finalize-pr.prompt.md +150 -0
- package/templates/shared/prompts/fix-issues.prompt.md +92 -0
- package/templates/shared/prompts/new-wp-component.prompt.md +51 -0
- package/templates/shared/prompts/new-wp-plugin.prompt.md +46 -0
- package/templates/shared/prompts/prepare-pr.prompt.md +123 -0
- package/templates/shared/prompts/prepare-release.prompt.md +74 -0
- package/templates/shared/prompts/quality-check.prompt.md +45 -0
- package/templates/shared/prompts/review-code.prompt.md +81 -0
- package/templates/shared/prompts/work-github-issue.prompt.md +96 -0
- package/templates/shared/prompts/work-ticket.prompt.md +98 -0
- package/templates/shared/skills/README.md +53 -0
- package/templates/shared/skills/component-architecture/SKILL.md +321 -0
- package/templates/shared/skills/create-component/SKILL.md +714 -0
- package/templates/shared/skills/domain-driven-design/SKILL.md +277 -0
- package/templates/shared/skills/plugin-creation/SKILL.md +1094 -0
- package/templates/shared/skills/quality-checks/SKILL.md +493 -0
- package/templates/shared/skills/release-management/SKILL.md +649 -0
- package/templates/shared/skills/testing/SKILL.md +682 -0
- package/templates/shared/skills/testing-patterns/SKILL.md +243 -0
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: create-component
|
|
3
|
+
description: Create new components in Silver Assist WordPress plugins following the LoadableInterface pattern. Covers adding services, controllers, views, models, repositories, and infrastructure components with proper registration, PHPDoc, and quality checks. Use when adding a new class or feature to an existing plugin.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Silver Assist — Create Component
|
|
7
|
+
|
|
8
|
+
This skill covers adding new components to an existing Silver Assist WordPress plugin. It provides templates, registration steps, and quality checks for each component type.
|
|
9
|
+
|
|
10
|
+
## When to Use
|
|
11
|
+
|
|
12
|
+
- Adding a new Service, Controller, View, Model, or Repository
|
|
13
|
+
- Creating a new admin page or feature
|
|
14
|
+
- Adding infrastructure components (ListTable, Widget, Handler)
|
|
15
|
+
- Registering a new component in `Plugin.php`
|
|
16
|
+
- Understanding which component type to use
|
|
17
|
+
|
|
18
|
+
## Component Type Decision Guide
|
|
19
|
+
|
|
20
|
+
| Need | Component Type | Priority | LoadableInterface? |
|
|
21
|
+
|------|---------------|----------|-------------------|
|
|
22
|
+
| Business logic, API calls, data processing | **Service** | 20 | Yes |
|
|
23
|
+
| Handle HTTP/admin requests, coordinate Service→View | **Controller** | 30 | Yes |
|
|
24
|
+
| Render HTML output | **View** | N/A | No (static class) |
|
|
25
|
+
| Represent data structures | **Model** | N/A | No (plain object) |
|
|
26
|
+
| Database queries, data access | **Repository** | N/A | No (used by Services) |
|
|
27
|
+
| WordPress integrations (WP_List_Table, widgets) | **Infrastructure** | 30 | Optional |
|
|
28
|
+
| Shared utilities | **Utils** | 40 | Yes |
|
|
29
|
+
| Plugin bootstrap, lifecycle | **Core** | 10 | Yes |
|
|
30
|
+
|
|
31
|
+
## Directory Structure
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
includes/
|
|
35
|
+
├── Core/ # Priority 10 — Bootstrap & lifecycle
|
|
36
|
+
│ ├── Plugin.php # Main plugin bootstrap (singleton)
|
|
37
|
+
│ ├── Activator.php # Activation/deactivation logic
|
|
38
|
+
│ └── Interfaces/
|
|
39
|
+
│ └── LoadableInterface.php
|
|
40
|
+
├── Service/ # Priority 20 — Business logic
|
|
41
|
+
│ ├── Loader.php # Service loader (optional)
|
|
42
|
+
│ └── Category/ # Group related services
|
|
43
|
+
│ └── ServiceName.php
|
|
44
|
+
├── Controller/ # Priority 30 — Request handlers
|
|
45
|
+
│ ├── Admin/ # Admin page controllers
|
|
46
|
+
│ └── Frontend/ # Frontend controllers
|
|
47
|
+
├── View/ # No priority — HTML rendering (static)
|
|
48
|
+
│ ├── Admin/ # Admin views
|
|
49
|
+
│ └── Frontend/ # Frontend views
|
|
50
|
+
├── Model/ # No priority — Domain models
|
|
51
|
+
├── Repository/ # No priority — Data access
|
|
52
|
+
├── Infrastructure/ # Priority 30 — WordPress integrations
|
|
53
|
+
│ ├── ListTable/
|
|
54
|
+
│ └── Widget/
|
|
55
|
+
├── Config/ # Configuration management
|
|
56
|
+
├── Exception/ # Custom exceptions
|
|
57
|
+
└── Utils/ # Priority 40 — Utilities
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Templates
|
|
63
|
+
|
|
64
|
+
### Service Class (Priority 20)
|
|
65
|
+
|
|
66
|
+
Services contain business logic. They implement `LoadableInterface` and use the singleton pattern.
|
|
67
|
+
|
|
68
|
+
```php
|
|
69
|
+
<?php
|
|
70
|
+
/**
|
|
71
|
+
* ServiceName Service
|
|
72
|
+
*
|
|
73
|
+
* Description of what this service does.
|
|
74
|
+
*
|
|
75
|
+
* @package SilverAssist\PluginName
|
|
76
|
+
* @subpackage Service\Category
|
|
77
|
+
* @since X.Y.Z
|
|
78
|
+
* @version X.Y.Z
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
namespace SilverAssist\PluginName\Service\Category;
|
|
82
|
+
|
|
83
|
+
use SilverAssist\PluginName\Core\Interfaces\LoadableInterface;
|
|
84
|
+
|
|
85
|
+
\defined( 'ABSPATH' ) || exit;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Class ServiceName
|
|
89
|
+
*
|
|
90
|
+
* Detailed description of the service's responsibility.
|
|
91
|
+
*
|
|
92
|
+
* @since X.Y.Z
|
|
93
|
+
*/
|
|
94
|
+
class ServiceName implements LoadableInterface {
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Singleton instance.
|
|
98
|
+
*
|
|
99
|
+
* @var self|null
|
|
100
|
+
*/
|
|
101
|
+
private static ?self $instance = null;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get singleton instance.
|
|
105
|
+
*
|
|
106
|
+
* @return self
|
|
107
|
+
*/
|
|
108
|
+
public static function instance(): self {
|
|
109
|
+
if ( null === self::$instance ) {
|
|
110
|
+
self::$instance = new self();
|
|
111
|
+
}
|
|
112
|
+
return self::$instance;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Private constructor for singleton.
|
|
117
|
+
*/
|
|
118
|
+
private function __construct() {}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Initialize the service.
|
|
122
|
+
*
|
|
123
|
+
* Register WordPress hooks and filters here.
|
|
124
|
+
*
|
|
125
|
+
* @return void
|
|
126
|
+
*/
|
|
127
|
+
public function init(): void {
|
|
128
|
+
// Register hooks here.
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get loading priority.
|
|
133
|
+
*
|
|
134
|
+
* @return int
|
|
135
|
+
*/
|
|
136
|
+
public function get_priority(): int {
|
|
137
|
+
return 20;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Check if this service should load.
|
|
142
|
+
*
|
|
143
|
+
* @return bool
|
|
144
|
+
*/
|
|
145
|
+
public function should_load(): bool {
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Controller Class (Priority 30)
|
|
152
|
+
|
|
153
|
+
Controllers handle HTTP/admin requests. They coordinate between Services (data) and Views (rendering).
|
|
154
|
+
|
|
155
|
+
**CRITICAL**: Controllers prepare ALL data and pass it to Views. Views never instantiate Services.
|
|
156
|
+
|
|
157
|
+
```php
|
|
158
|
+
<?php
|
|
159
|
+
/**
|
|
160
|
+
* ControllerName Controller
|
|
161
|
+
*
|
|
162
|
+
* Handles requests for feature.
|
|
163
|
+
*
|
|
164
|
+
* @package SilverAssist\PluginName
|
|
165
|
+
* @subpackage Controller\Admin
|
|
166
|
+
* @since X.Y.Z
|
|
167
|
+
* @version X.Y.Z
|
|
168
|
+
*/
|
|
169
|
+
|
|
170
|
+
namespace SilverAssist\PluginName\Controller\Admin;
|
|
171
|
+
|
|
172
|
+
use SilverAssist\PluginName\Core\Interfaces\LoadableInterface;
|
|
173
|
+
use SilverAssist\PluginName\Service\Category\ServiceName;
|
|
174
|
+
use SilverAssist\PluginName\View\Admin\ViewName;
|
|
175
|
+
|
|
176
|
+
\defined( 'ABSPATH' ) || exit;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Class ControllerName
|
|
180
|
+
*
|
|
181
|
+
* Coordinates Service → View for feature.
|
|
182
|
+
*
|
|
183
|
+
* @since X.Y.Z
|
|
184
|
+
*/
|
|
185
|
+
class ControllerName implements LoadableInterface {
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Singleton instance.
|
|
189
|
+
*
|
|
190
|
+
* @var self|null
|
|
191
|
+
*/
|
|
192
|
+
private static ?self $instance = null;
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Service instance.
|
|
196
|
+
*
|
|
197
|
+
* @var ServiceName
|
|
198
|
+
*/
|
|
199
|
+
private ServiceName $service;
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Get singleton instance.
|
|
203
|
+
*
|
|
204
|
+
* @return self
|
|
205
|
+
*/
|
|
206
|
+
public static function instance(): self {
|
|
207
|
+
if ( null === self::$instance ) {
|
|
208
|
+
self::$instance = new self();
|
|
209
|
+
}
|
|
210
|
+
return self::$instance;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Private constructor for singleton.
|
|
215
|
+
*/
|
|
216
|
+
private function __construct() {
|
|
217
|
+
$this->service = ServiceName::instance();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Initialize controller.
|
|
222
|
+
*
|
|
223
|
+
* @return void
|
|
224
|
+
*/
|
|
225
|
+
public function init(): void {
|
|
226
|
+
\add_action( 'admin_menu', [ $this, 'register_page' ] );
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Get loading priority.
|
|
231
|
+
*
|
|
232
|
+
* @return int
|
|
233
|
+
*/
|
|
234
|
+
public function get_priority(): int {
|
|
235
|
+
return 30;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Check if should load.
|
|
240
|
+
*
|
|
241
|
+
* @return bool
|
|
242
|
+
*/
|
|
243
|
+
public function should_load(): bool {
|
|
244
|
+
return \is_admin();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Register admin page.
|
|
249
|
+
*
|
|
250
|
+
* @return void
|
|
251
|
+
*/
|
|
252
|
+
public function register_page(): void {
|
|
253
|
+
\add_submenu_page(
|
|
254
|
+
'parent-slug',
|
|
255
|
+
\__( 'Page Title', 'plugin-text-domain' ),
|
|
256
|
+
\__( 'Menu Title', 'plugin-text-domain' ),
|
|
257
|
+
'manage_options',
|
|
258
|
+
'page-slug',
|
|
259
|
+
[ $this, 'render_page' ]
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Render page.
|
|
265
|
+
*
|
|
266
|
+
* Controller prepares all data, then delegates rendering to the View.
|
|
267
|
+
*
|
|
268
|
+
* @return void
|
|
269
|
+
*/
|
|
270
|
+
public function render_page(): void {
|
|
271
|
+
// ✅ Controller prepares ALL data.
|
|
272
|
+
$data = $this->service->get_data();
|
|
273
|
+
|
|
274
|
+
// ✅ Pass prepared data to static View.
|
|
275
|
+
ViewName::render( $data );
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### View Class (Static — No LoadableInterface)
|
|
281
|
+
|
|
282
|
+
Views are static classes that render HTML. They NEVER instantiate Services or access the database directly.
|
|
283
|
+
|
|
284
|
+
**CRITICAL**: All data must be passed as parameters from the Controller.
|
|
285
|
+
|
|
286
|
+
```php
|
|
287
|
+
<?php
|
|
288
|
+
/**
|
|
289
|
+
* ViewName View
|
|
290
|
+
*
|
|
291
|
+
* HTML rendering for feature.
|
|
292
|
+
* NOTE: Views receive all data as parameters — never instantiate Services here.
|
|
293
|
+
*
|
|
294
|
+
* @package SilverAssist\PluginName
|
|
295
|
+
* @subpackage View\Admin
|
|
296
|
+
* @since X.Y.Z
|
|
297
|
+
* @version X.Y.Z
|
|
298
|
+
*/
|
|
299
|
+
|
|
300
|
+
namespace SilverAssist\PluginName\View\Admin;
|
|
301
|
+
|
|
302
|
+
\defined( 'ABSPATH' ) || exit;
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Class ViewName
|
|
306
|
+
*
|
|
307
|
+
* Renders HTML for feature.
|
|
308
|
+
*
|
|
309
|
+
* @since X.Y.Z
|
|
310
|
+
*/
|
|
311
|
+
class ViewName {
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Render the main view.
|
|
315
|
+
*
|
|
316
|
+
* All data must be passed as parameters from the Controller.
|
|
317
|
+
* Views should NEVER instantiate Services directly.
|
|
318
|
+
*
|
|
319
|
+
* @param array<string, mixed> $data Data to render (prepared by Controller).
|
|
320
|
+
* @return void
|
|
321
|
+
*/
|
|
322
|
+
public static function render( array $data ): void {
|
|
323
|
+
?>
|
|
324
|
+
<div class="wrap">
|
|
325
|
+
<h1><?php \esc_html_e( 'Page Title', 'plugin-text-domain' ); ?></h1>
|
|
326
|
+
<?php if ( ! empty( $data ) ) : ?>
|
|
327
|
+
<!-- Render data using proper escaping -->
|
|
328
|
+
<p><?php echo \esc_html( $data['message'] ?? '' ); ?></p>
|
|
329
|
+
<?php endif; ?>
|
|
330
|
+
</div>
|
|
331
|
+
<?php
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Model Class (No LoadableInterface)
|
|
337
|
+
|
|
338
|
+
Models represent data structures. They are plain PHP objects with typed properties.
|
|
339
|
+
|
|
340
|
+
```php
|
|
341
|
+
<?php
|
|
342
|
+
/**
|
|
343
|
+
* ModelName Model
|
|
344
|
+
*
|
|
345
|
+
* Represents a data entity.
|
|
346
|
+
*
|
|
347
|
+
* @package SilverAssist\PluginName
|
|
348
|
+
* @subpackage Model
|
|
349
|
+
* @since X.Y.Z
|
|
350
|
+
* @version X.Y.Z
|
|
351
|
+
*/
|
|
352
|
+
|
|
353
|
+
namespace SilverAssist\PluginName\Model;
|
|
354
|
+
|
|
355
|
+
\defined( 'ABSPATH' ) || exit;
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Class ModelName
|
|
359
|
+
*
|
|
360
|
+
* Data transfer object for entity.
|
|
361
|
+
*
|
|
362
|
+
* @since X.Y.Z
|
|
363
|
+
*/
|
|
364
|
+
class ModelName {
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Constructor.
|
|
368
|
+
*
|
|
369
|
+
* @param int $id Entity ID.
|
|
370
|
+
* @param string $name Entity name.
|
|
371
|
+
* @param string $status Entity status.
|
|
372
|
+
*/
|
|
373
|
+
public function __construct(
|
|
374
|
+
public readonly int $id,
|
|
375
|
+
public readonly string $name,
|
|
376
|
+
public readonly string $status = 'active',
|
|
377
|
+
) {}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Create from database row.
|
|
381
|
+
*
|
|
382
|
+
* @param object $row Database row object.
|
|
383
|
+
* @return self
|
|
384
|
+
*/
|
|
385
|
+
public static function from_row( object $row ): self {
|
|
386
|
+
return new self(
|
|
387
|
+
id: (int) $row->id,
|
|
388
|
+
name: (string) $row->name,
|
|
389
|
+
status: (string) ( $row->status ?? 'active' ),
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Convert to array.
|
|
395
|
+
*
|
|
396
|
+
* @return array<string, mixed>
|
|
397
|
+
*/
|
|
398
|
+
public function to_array(): array {
|
|
399
|
+
return [
|
|
400
|
+
'id' => $this->id,
|
|
401
|
+
'name' => $this->name,
|
|
402
|
+
'status' => $this->status,
|
|
403
|
+
];
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### Repository Class (No LoadableInterface)
|
|
409
|
+
|
|
410
|
+
Repositories handle database queries. They are used by Services.
|
|
411
|
+
|
|
412
|
+
```php
|
|
413
|
+
<?php
|
|
414
|
+
/**
|
|
415
|
+
* ModelName Repository
|
|
416
|
+
*
|
|
417
|
+
* Data access for entity.
|
|
418
|
+
*
|
|
419
|
+
* @package SilverAssist\PluginName
|
|
420
|
+
* @subpackage Repository
|
|
421
|
+
* @since X.Y.Z
|
|
422
|
+
* @version X.Y.Z
|
|
423
|
+
*/
|
|
424
|
+
|
|
425
|
+
namespace SilverAssist\PluginName\Repository;
|
|
426
|
+
|
|
427
|
+
use SilverAssist\PluginName\Model\ModelName;
|
|
428
|
+
|
|
429
|
+
\defined( 'ABSPATH' ) || exit;
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Class ModelNameRepository
|
|
433
|
+
*
|
|
434
|
+
* Handles database operations for entity.
|
|
435
|
+
*
|
|
436
|
+
* @since X.Y.Z
|
|
437
|
+
*/
|
|
438
|
+
class ModelNameRepository {
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Table name (without prefix).
|
|
442
|
+
*
|
|
443
|
+
* @var string
|
|
444
|
+
*/
|
|
445
|
+
private string $table;
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Constructor.
|
|
449
|
+
*/
|
|
450
|
+
public function __construct() {
|
|
451
|
+
global $wpdb;
|
|
452
|
+
$this->table = $wpdb->prefix . 'plugin_prefix_table';
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Find by ID.
|
|
457
|
+
*
|
|
458
|
+
* @param int $id Entity ID.
|
|
459
|
+
* @return ModelName|null
|
|
460
|
+
*/
|
|
461
|
+
public function find( int $id ): ?ModelName {
|
|
462
|
+
global $wpdb;
|
|
463
|
+
|
|
464
|
+
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
|
465
|
+
$row = $wpdb->get_row(
|
|
466
|
+
$wpdb->prepare(
|
|
467
|
+
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
468
|
+
"SELECT * FROM {$this->table} WHERE id = %d",
|
|
469
|
+
$id
|
|
470
|
+
)
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
if ( ! $row ) {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return ModelName::from_row( $row );
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Find all with pagination.
|
|
482
|
+
*
|
|
483
|
+
* @param int $per_page Items per page.
|
|
484
|
+
* @param int $page Page number.
|
|
485
|
+
* @return array<int, ModelName>
|
|
486
|
+
*/
|
|
487
|
+
public function find_all( int $per_page = 20, int $page = 1 ): array {
|
|
488
|
+
global $wpdb;
|
|
489
|
+
|
|
490
|
+
$offset = ( $page - 1 ) * $per_page;
|
|
491
|
+
|
|
492
|
+
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
|
493
|
+
$rows = $wpdb->get_results(
|
|
494
|
+
$wpdb->prepare(
|
|
495
|
+
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
496
|
+
"SELECT * FROM {$this->table} ORDER BY id DESC LIMIT %d OFFSET %d",
|
|
497
|
+
$per_page,
|
|
498
|
+
$offset
|
|
499
|
+
)
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
return array_map(
|
|
503
|
+
fn( object $row ) => ModelName::from_row( $row ),
|
|
504
|
+
$rows
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Save entity.
|
|
510
|
+
*
|
|
511
|
+
* @param ModelName $model Entity to save.
|
|
512
|
+
* @return int|false Inserted ID or false on failure.
|
|
513
|
+
*/
|
|
514
|
+
public function save( ModelName $model ): int|false {
|
|
515
|
+
global $wpdb;
|
|
516
|
+
|
|
517
|
+
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
|
518
|
+
$result = $wpdb->insert(
|
|
519
|
+
$this->table,
|
|
520
|
+
$model->to_array(),
|
|
521
|
+
[ '%d', '%s', '%s' ]
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
return $result ? (int) $wpdb->insert_id : false;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Delete by ID.
|
|
529
|
+
*
|
|
530
|
+
* @param int $id Entity ID.
|
|
531
|
+
* @return bool
|
|
532
|
+
*/
|
|
533
|
+
public function delete( int $id ): bool {
|
|
534
|
+
global $wpdb;
|
|
535
|
+
|
|
536
|
+
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
|
537
|
+
$result = $wpdb->delete(
|
|
538
|
+
$this->table,
|
|
539
|
+
[ 'id' => $id ],
|
|
540
|
+
[ '%d' ]
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
return (bool) $result;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
---
|
|
549
|
+
|
|
550
|
+
## Registration Steps
|
|
551
|
+
|
|
552
|
+
After creating a component class that implements `LoadableInterface`:
|
|
553
|
+
|
|
554
|
+
### 1. Register in Plugin.php
|
|
555
|
+
|
|
556
|
+
Edit `includes/Core/Plugin.php` — add the class to `get_components()`:
|
|
557
|
+
|
|
558
|
+
```php
|
|
559
|
+
private function get_components(): array {
|
|
560
|
+
return [
|
|
561
|
+
// Core - Priority 10.
|
|
562
|
+
\SilverAssist\PluginName\Core\Activator::class,
|
|
563
|
+
|
|
564
|
+
// Services - Priority 20.
|
|
565
|
+
\SilverAssist\PluginName\Service\Category\ServiceName::class, // ← NEW
|
|
566
|
+
|
|
567
|
+
// Controllers - Priority 30.
|
|
568
|
+
\SilverAssist\PluginName\Controller\Admin\ControllerName::class, // ← NEW
|
|
569
|
+
|
|
570
|
+
// Utils - Priority 40.
|
|
571
|
+
];
|
|
572
|
+
}
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
**Rules:**
|
|
576
|
+
- Group by priority tier with comments.
|
|
577
|
+
- Keep entries alphabetically within each group.
|
|
578
|
+
- Only add classes that implement `LoadableInterface`.
|
|
579
|
+
- Models, Views, Repositories, and Exceptions are NOT registered here.
|
|
580
|
+
|
|
581
|
+
### 2. Create Matching Test File
|
|
582
|
+
|
|
583
|
+
Test file mirrors the source file path under `tests/Unit/`:
|
|
584
|
+
|
|
585
|
+
| Source | Test |
|
|
586
|
+
|--------|------|
|
|
587
|
+
| `includes/Service/Api/ApiClient.php` | `tests/Unit/Service/Api/ApiClientTest.php` |
|
|
588
|
+
| `includes/Controller/Admin/LogsController.php` | `tests/Unit/Controller/Admin/LogsControllerTest.php` |
|
|
589
|
+
| `includes/Model/LogEntry.php` | `tests/Unit/Model/LogEntryTest.php` |
|
|
590
|
+
|
|
591
|
+
```php
|
|
592
|
+
<?php
|
|
593
|
+
namespace SilverAssist\PluginName\Tests\Unit\Service\Category;
|
|
594
|
+
|
|
595
|
+
use SilverAssist\PluginName\Tests\Helpers\TestCase;
|
|
596
|
+
use SilverAssist\PluginName\Service\Category\ServiceName;
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Tests for ServiceName.
|
|
600
|
+
*
|
|
601
|
+
* @covers \SilverAssist\PluginName\Service\Category\ServiceName
|
|
602
|
+
*/
|
|
603
|
+
class ServiceNameTest extends TestCase {
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Service instance.
|
|
607
|
+
*
|
|
608
|
+
* @var ServiceName
|
|
609
|
+
*/
|
|
610
|
+
private ServiceName $service;
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Set up test fixtures.
|
|
614
|
+
*
|
|
615
|
+
* @return void
|
|
616
|
+
*/
|
|
617
|
+
public function set_up(): void {
|
|
618
|
+
parent::set_up();
|
|
619
|
+
$this->service = ServiceName::instance();
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Test singleton returns same instance.
|
|
624
|
+
*
|
|
625
|
+
* @return void
|
|
626
|
+
*/
|
|
627
|
+
public function testInstanceReturnsSameObject(): void {
|
|
628
|
+
$instance1 = ServiceName::instance();
|
|
629
|
+
$instance2 = ServiceName::instance();
|
|
630
|
+
$this->assertSame( $instance1, $instance2 );
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Test get_priority returns expected value.
|
|
635
|
+
*
|
|
636
|
+
* @return void
|
|
637
|
+
*/
|
|
638
|
+
public function testGetPriorityReturnsExpectedValue(): void {
|
|
639
|
+
$this->assertSame( 20, $this->service->get_priority() );
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Test should_load returns true.
|
|
644
|
+
*
|
|
645
|
+
* @return void
|
|
646
|
+
*/
|
|
647
|
+
public function testShouldLoadReturnsTrue(): void {
|
|
648
|
+
$this->assertTrue( $this->service->should_load() );
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
### 3. Run Quality Checks
|
|
654
|
+
|
|
655
|
+
```bash
|
|
656
|
+
# Check PHPCS compliance for the new file.
|
|
657
|
+
vendor/bin/phpcs includes/Service/Category/ServiceName.php
|
|
658
|
+
|
|
659
|
+
# Run PHPStan on the new file.
|
|
660
|
+
php -d memory_limit=512M vendor/bin/phpstan analyse includes/Service/Category/ServiceName.php --level=8
|
|
661
|
+
|
|
662
|
+
# Run the matching test.
|
|
663
|
+
vendor/bin/phpunit --filter ServiceNameTest
|
|
664
|
+
|
|
665
|
+
# Full quality check.
|
|
666
|
+
./scripts/run-quality-checks.sh
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
---
|
|
670
|
+
|
|
671
|
+
## MVC Flow Pattern
|
|
672
|
+
|
|
673
|
+
The correct data flow in Silver Assist plugins is:
|
|
674
|
+
|
|
675
|
+
```
|
|
676
|
+
User Request → Controller → Service → Repository/WordPress API
|
|
677
|
+
↓
|
|
678
|
+
View::render($data) ← Static call with prepared data
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
**Rules:**
|
|
682
|
+
1. **Controller** receives the request, calls Service methods, prepares data, passes to View.
|
|
683
|
+
2. **Service** contains business logic, calls Repositories or WordPress APIs.
|
|
684
|
+
3. **View** renders HTML using ONLY the data passed as parameters.
|
|
685
|
+
4. **Model** is a data structure used to transfer data between layers.
|
|
686
|
+
5. **Repository** handles database operations, returns Models.
|
|
687
|
+
|
|
688
|
+
**Anti-patterns to avoid:**
|
|
689
|
+
- ❌ View instantiating a Service: `$service = ServiceName::instance();`
|
|
690
|
+
- ❌ View making database queries directly.
|
|
691
|
+
- ❌ Controller containing business logic (should delegate to Service).
|
|
692
|
+
- ❌ Service rendering HTML (should delegate to View via Controller).
|
|
693
|
+
|
|
694
|
+
---
|
|
695
|
+
|
|
696
|
+
## Component Checklist
|
|
697
|
+
|
|
698
|
+
Before submitting a PR with a new component:
|
|
699
|
+
|
|
700
|
+
- [ ] Follows namespace convention `SilverAssist\PluginName\`
|
|
701
|
+
- [ ] File path matches PSR-4 autoloading (PascalCase)
|
|
702
|
+
- [ ] Implements `LoadableInterface` (if needs initialization)
|
|
703
|
+
- [ ] Uses singleton pattern with `instance()` method
|
|
704
|
+
- [ ] Has complete PHPDoc with `@package`, `@since`, `@version`
|
|
705
|
+
- [ ] Uses `\` prefix for WordPress functions in namespaced code
|
|
706
|
+
- [ ] Uses `\defined( 'ABSPATH' ) || exit;` security guard
|
|
707
|
+
- [ ] Single quotes for simple strings, double for interpolation
|
|
708
|
+
- [ ] Registered in `Plugin.php::get_components()` (if LoadableInterface)
|
|
709
|
+
- [ ] Matching test file created in `tests/Unit/`
|
|
710
|
+
- [ ] Views do NOT instantiate Services (data passed from Controller)
|
|
711
|
+
- [ ] Controllers prepare all data before passing to Views
|
|
712
|
+
- [ ] Passes `vendor/bin/phpcs`
|
|
713
|
+
- [ ] Passes `vendor/bin/phpstan analyse --level=8`
|
|
714
|
+
- [ ] Test passes with `vendor/bin/phpunit --filter ComponentNameTest`
|