@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,264 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: PHPUnit testing standards, conventions, and patterns for Silver Assist WordPress plugins using WP_UnitTestCase
|
|
3
|
+
name: Testing Standards
|
|
4
|
+
applyTo: "tests/**/*.php"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# PHPUnit Testing Standards — Silver Assist Plugins
|
|
8
|
+
|
|
9
|
+
**Applies to**: All test files in Silver Assist WordPress plugins
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Must-Follow Rules
|
|
14
|
+
|
|
15
|
+
### 1. Base Class: All Tests Extend TestCase (WP_UnitTestCase)
|
|
16
|
+
|
|
17
|
+
```php
|
|
18
|
+
use SilverAssist\PluginName\Tests\Helpers\TestCase;
|
|
19
|
+
|
|
20
|
+
class MyTest extends TestCase {
|
|
21
|
+
// Tests here.
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### 2. Lifecycle Methods Use WordPress snake_case
|
|
26
|
+
|
|
27
|
+
Since TestCase extends WP_UnitTestCase, use WordPress-style snake_case:
|
|
28
|
+
|
|
29
|
+
```php
|
|
30
|
+
// ✅ CORRECT — WordPress snake_case (WP_UnitTestCase).
|
|
31
|
+
public function set_up(): void
|
|
32
|
+
public function tear_down(): void
|
|
33
|
+
public static function set_up_before_class(): void
|
|
34
|
+
public static function tear_down_after_class(): void
|
|
35
|
+
|
|
36
|
+
// ❌ WRONG — PHPUnit camelCase (don't use with WP_UnitTestCase).
|
|
37
|
+
public function setUp(): void
|
|
38
|
+
public function tearDown(): void
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 3. Test Method Naming — camelCase
|
|
42
|
+
|
|
43
|
+
```php
|
|
44
|
+
// ✅ CORRECT — Descriptive camelCase test names.
|
|
45
|
+
public function testStartRequestCreatesLogEntry(): void
|
|
46
|
+
public function testSearchMatchesSenderName(): void
|
|
47
|
+
|
|
48
|
+
// ❌ WRONG — Vague names.
|
|
49
|
+
public function testMethod(): void
|
|
50
|
+
public function testItWorks(): void
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 4. Database Queries: Use %i for Table Names
|
|
54
|
+
|
|
55
|
+
```php
|
|
56
|
+
// ✅ CORRECT
|
|
57
|
+
$wpdb->prepare( 'SELECT * FROM %i WHERE id = %d', $table_name, $id );
|
|
58
|
+
|
|
59
|
+
// ❌ WRONG — Variable interpolation.
|
|
60
|
+
$wpdb->prepare( "SELECT * FROM {$table_name} WHERE id = %d", $id );
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 5. Skip When Dependencies Missing
|
|
64
|
+
|
|
65
|
+
```php
|
|
66
|
+
public function set_up(): void {
|
|
67
|
+
parent::set_up();
|
|
68
|
+
|
|
69
|
+
if ( ! class_exists( 'WPCF7_ContactForm' ) ) {
|
|
70
|
+
$this->markTestSkipped( 'Contact Form 7 not loaded' );
|
|
71
|
+
return; // IMPORTANT: always return after skip.
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 6. Always Clean Up Resources
|
|
77
|
+
|
|
78
|
+
```php
|
|
79
|
+
public function tear_down(): void {
|
|
80
|
+
$this->service = null;
|
|
81
|
+
parent::tear_down();
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 7. Use PHPDoc Group Annotations
|
|
86
|
+
|
|
87
|
+
```php
|
|
88
|
+
/**
|
|
89
|
+
* Tests for ClassName.
|
|
90
|
+
*
|
|
91
|
+
* @group unit
|
|
92
|
+
* @group logging
|
|
93
|
+
* @covers \SilverAssist\PluginName\Service\ClassName
|
|
94
|
+
*/
|
|
95
|
+
class ClassNameTest extends TestCase {
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Test Structure Template
|
|
102
|
+
|
|
103
|
+
```php
|
|
104
|
+
<?php
|
|
105
|
+
/**
|
|
106
|
+
* Tests for ClassName.
|
|
107
|
+
*
|
|
108
|
+
* @package SilverAssist\PluginName\Tests\Unit
|
|
109
|
+
*/
|
|
110
|
+
|
|
111
|
+
namespace SilverAssist\PluginName\Tests\Unit\Service\Category;
|
|
112
|
+
|
|
113
|
+
use SilverAssist\PluginName\Tests\Helpers\TestCase;
|
|
114
|
+
use SilverAssist\PluginName\Service\Category\ClassName;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* ClassName test case.
|
|
118
|
+
*
|
|
119
|
+
* @group unit
|
|
120
|
+
* @group service
|
|
121
|
+
* @covers \SilverAssist\PluginName\Service\Category\ClassName
|
|
122
|
+
*/
|
|
123
|
+
class ClassNameTest extends TestCase {
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Instance under test.
|
|
127
|
+
*
|
|
128
|
+
* @var ClassName|null
|
|
129
|
+
*/
|
|
130
|
+
private ?ClassName $instance = null;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Set up test fixtures.
|
|
134
|
+
*/
|
|
135
|
+
public function set_up(): void {
|
|
136
|
+
parent::set_up();
|
|
137
|
+
$this->instance = new ClassName();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Tear down test fixtures.
|
|
142
|
+
*/
|
|
143
|
+
public function tear_down(): void {
|
|
144
|
+
$this->instance = null;
|
|
145
|
+
parent::tear_down();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Test that method returns expected value.
|
|
150
|
+
*/
|
|
151
|
+
public function testMethodReturnsExpectedValue(): void {
|
|
152
|
+
$result = $this->instance->method( 'input' );
|
|
153
|
+
$this->assertSame( 'expected', $result );
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Data Provider Pattern
|
|
161
|
+
|
|
162
|
+
```php
|
|
163
|
+
/**
|
|
164
|
+
* Data provider for search tests.
|
|
165
|
+
*
|
|
166
|
+
* @return array<string, array{item: array<string, mixed>, search: string, expected: bool}>
|
|
167
|
+
*/
|
|
168
|
+
public static function searchDataProvider(): array {
|
|
169
|
+
return [
|
|
170
|
+
'exact match' => [
|
|
171
|
+
'item' => [ 'name' => 'John Doe' ],
|
|
172
|
+
'search' => 'john',
|
|
173
|
+
'expected' => true,
|
|
174
|
+
],
|
|
175
|
+
'no match' => [
|
|
176
|
+
'item' => [ 'name' => 'John Doe' ],
|
|
177
|
+
'search' => 'jane',
|
|
178
|
+
'expected' => false,
|
|
179
|
+
],
|
|
180
|
+
];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* @dataProvider searchDataProvider
|
|
185
|
+
* @param array<string, mixed> $item Item to search.
|
|
186
|
+
* @param string $search Search term.
|
|
187
|
+
* @param bool $expected Expected result.
|
|
188
|
+
*/
|
|
189
|
+
public function testSearchMatching( array $item, string $search, bool $expected ): void {
|
|
190
|
+
$result = $this->instance->matches_search( $item, $search );
|
|
191
|
+
$this->assertSame( $expected, $result );
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Assertion Best Practices
|
|
198
|
+
|
|
199
|
+
```php
|
|
200
|
+
// ✅ Use specific assertions.
|
|
201
|
+
$this->assertSame( 'expected', $actual ); // Strict type + value.
|
|
202
|
+
$this->assertEquals( 5, $count ); // Value equality.
|
|
203
|
+
$this->assertTrue( $result );
|
|
204
|
+
$this->assertNull( $value );
|
|
205
|
+
$this->assertCount( 3, $array );
|
|
206
|
+
$this->assertArrayHasKey( 'key', $array );
|
|
207
|
+
$this->assertInstanceOf( ClassName::class, $obj );
|
|
208
|
+
|
|
209
|
+
// ❌ Avoid generic assertions.
|
|
210
|
+
$this->assertTrue( $result === 'expected' ); // Use assertSame.
|
|
211
|
+
$this->assertTrue( count( $array ) === 3 ); // Use assertCount.
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Add Assertion Messages
|
|
215
|
+
|
|
216
|
+
```php
|
|
217
|
+
$this->assertSame(
|
|
218
|
+
3,
|
|
219
|
+
$result['total'],
|
|
220
|
+
'Should count 3 total errors (700, 701, 703)'
|
|
221
|
+
);
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## WordPress Factory Pattern
|
|
227
|
+
|
|
228
|
+
```php
|
|
229
|
+
public function testWithFactory(): void {
|
|
230
|
+
$user_id = $this->factory->user->create( [
|
|
231
|
+
'role' => 'administrator',
|
|
232
|
+
] );
|
|
233
|
+
|
|
234
|
+
$post_id = $this->factory->post->create( [
|
|
235
|
+
'post_author' => $user_id,
|
|
236
|
+
'post_title' => 'Test Post',
|
|
237
|
+
] );
|
|
238
|
+
|
|
239
|
+
wp_set_current_user( $user_id );
|
|
240
|
+
$this->assertTrue( current_user_can( 'manage_options' ) );
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## Running Tests
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
# Run all tests.
|
|
250
|
+
vendor/bin/phpunit
|
|
251
|
+
|
|
252
|
+
# Run specific test file.
|
|
253
|
+
vendor/bin/phpunit tests/Unit/Service/ClassNameTest.php
|
|
254
|
+
|
|
255
|
+
# Run specific test method.
|
|
256
|
+
vendor/bin/phpunit --filter testMethodName
|
|
257
|
+
|
|
258
|
+
# Run tests by group.
|
|
259
|
+
vendor/bin/phpunit --group unit
|
|
260
|
+
vendor/bin/phpunit --group integration
|
|
261
|
+
|
|
262
|
+
# Run with coverage.
|
|
263
|
+
vendor/bin/phpunit --coverage-text
|
|
264
|
+
```
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "**/*.test.{ts,tsx}"
|
|
3
|
+
---
|
|
4
|
+
# Testing Standards
|
|
5
|
+
|
|
6
|
+
## Test File Location
|
|
7
|
+
|
|
8
|
+
Place tests in `__tests__/` folders next to the code:
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
components/
|
|
12
|
+
└── button/
|
|
13
|
+
├── index.tsx
|
|
14
|
+
└── __tests__/
|
|
15
|
+
└── button.test.tsx
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Test Structure
|
|
19
|
+
|
|
20
|
+
Use `describe` and `it` blocks:
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { render, screen } from '@testing-library/react';
|
|
24
|
+
import userEvent from '@testing-library/user-event';
|
|
25
|
+
import { Button } from '../index';
|
|
26
|
+
|
|
27
|
+
describe('Button', () => {
|
|
28
|
+
const defaultProps = {
|
|
29
|
+
onClick: jest.fn(),
|
|
30
|
+
children: 'Click me',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
jest.clearAllMocks();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('rendering', () => {
|
|
38
|
+
it('renders children correctly', () => {
|
|
39
|
+
render(<Button {...defaultProps} />);
|
|
40
|
+
expect(screen.getByText('Click me')).toBeInTheDocument();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('behavior', () => {
|
|
45
|
+
it('calls onClick when clicked', async () => {
|
|
46
|
+
const user = userEvent.setup();
|
|
47
|
+
render(<Button {...defaultProps} />);
|
|
48
|
+
|
|
49
|
+
await user.click(screen.getByRole('button'));
|
|
50
|
+
|
|
51
|
+
expect(defaultProps.onClick).toHaveBeenCalledTimes(1);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Test Categories
|
|
58
|
+
|
|
59
|
+
Include tests for:
|
|
60
|
+
|
|
61
|
+
### Rendering
|
|
62
|
+
- Component renders without crashing
|
|
63
|
+
- All expected elements are present
|
|
64
|
+
- Conditional rendering works
|
|
65
|
+
|
|
66
|
+
### Props
|
|
67
|
+
- Default props work correctly
|
|
68
|
+
- Custom props are applied
|
|
69
|
+
- Required props are validated
|
|
70
|
+
|
|
71
|
+
### User Interactions
|
|
72
|
+
- Click handlers work
|
|
73
|
+
- Form inputs update
|
|
74
|
+
- Keyboard navigation
|
|
75
|
+
|
|
76
|
+
### Edge Cases
|
|
77
|
+
- Empty data handled
|
|
78
|
+
- Null/undefined handled
|
|
79
|
+
- Error states displayed
|
|
80
|
+
|
|
81
|
+
## Mocking
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
// Mock a module
|
|
85
|
+
jest.mock('@/lib/api', () => ({
|
|
86
|
+
fetchData: jest.fn(),
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
// Mock a hook
|
|
90
|
+
jest.mock('@/hooks/use-user', () => ({
|
|
91
|
+
useUser: () => ({ user: { name: 'Test' }, isLoading: false }),
|
|
92
|
+
}));
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Assertions
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
// Prefer specific assertions
|
|
99
|
+
expect(screen.getByRole('button')).toBeInTheDocument();
|
|
100
|
+
expect(screen.getByText('Hello')).toBeVisible();
|
|
101
|
+
expect(element).toHaveClass('active');
|
|
102
|
+
expect(handler).toHaveBeenCalledWith(expectedArgs);
|
|
103
|
+
```
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "**/*.{ts,tsx}"
|
|
3
|
+
---
|
|
4
|
+
# TypeScript Code Style Standards
|
|
5
|
+
|
|
6
|
+
## Export Rules
|
|
7
|
+
|
|
8
|
+
**Components** use `export default`, everything else uses **named exports**:
|
|
9
|
+
|
|
10
|
+
```typescript
|
|
11
|
+
// ✅ Components: default export
|
|
12
|
+
// components/auth/login-form/index.tsx
|
|
13
|
+
export default function LoginForm() { }
|
|
14
|
+
|
|
15
|
+
// ✅ Everything else: named exports
|
|
16
|
+
// lib/utils.ts
|
|
17
|
+
export function formatDate(date: Date): string { }
|
|
18
|
+
export function validateEmail(email: string): boolean { }
|
|
19
|
+
|
|
20
|
+
// types/user.ts
|
|
21
|
+
export interface User { }
|
|
22
|
+
export type UserRole = "admin" | "user";
|
|
23
|
+
|
|
24
|
+
// actions/auth/login.ts
|
|
25
|
+
export async function login(formData: FormData) { }
|
|
26
|
+
|
|
27
|
+
// hooks/use-form.ts
|
|
28
|
+
export function useForm() { }
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Avoid Nested Ternaries
|
|
32
|
+
**❌ NEVER use nested ternary operators** - they reduce readability significantly.
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
// ❌ INCORRECT: Nested ternaries
|
|
36
|
+
const status = isLoading ? 'loading' : hasError ? 'error' : 'success';
|
|
37
|
+
|
|
38
|
+
// ✅ CORRECT: Use if/else or early returns
|
|
39
|
+
function getStatus(isLoading: boolean, hasError: boolean): string {
|
|
40
|
+
if (isLoading) return 'loading';
|
|
41
|
+
if (hasError) return 'error';
|
|
42
|
+
return 'success';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ✅ CORRECT: Simple ternary is OK (no nesting)
|
|
46
|
+
const greeting = isLoggedIn ? 'Welcome back!' : 'Please log in';
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Clarity Over Brevity
|
|
50
|
+
|
|
51
|
+
**✅ PREFER explicit, readable code** over compact one-liners.
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
// ❌ INCORRECT: Too compact, hard to understand
|
|
55
|
+
const result = data?.items?.filter(i => i.active).map(i => i.id).join(',') || '';
|
|
56
|
+
|
|
57
|
+
// ✅ CORRECT: Clear and maintainable
|
|
58
|
+
const activeItems = data?.items?.filter(item => item.active) ?? [];
|
|
59
|
+
const itemIds = activeItems.map(item => item.id);
|
|
60
|
+
const result = itemIds.join(',');
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Object Destructuring
|
|
64
|
+
|
|
65
|
+
**✅ ALWAYS use destructuring** for function parameters and object properties.
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// ✅ CORRECT: Destructure at the start
|
|
69
|
+
function processData(data: DataType) {
|
|
70
|
+
const { name, email, phone, address } = data;
|
|
71
|
+
console.log(email);
|
|
72
|
+
sendEmail(email);
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## No `any` Type
|
|
77
|
+
|
|
78
|
+
**❌ NEVER use `any`** - always define explicit types.
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
// ❌ INCORRECT
|
|
82
|
+
function processData(data: any) { ... }
|
|
83
|
+
|
|
84
|
+
// ✅ CORRECT
|
|
85
|
+
interface DataType {
|
|
86
|
+
name: string;
|
|
87
|
+
email: string;
|
|
88
|
+
}
|
|
89
|
+
function processData(data: DataType) { ... }
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## JSDoc Comments
|
|
93
|
+
|
|
94
|
+
**✅ ALWAYS add JSDoc** to public functions.
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
/**
|
|
98
|
+
* Formats a date for display.
|
|
99
|
+
* @param date - The date to format
|
|
100
|
+
* @param locale - The locale (default: 'en-US')
|
|
101
|
+
* @returns Formatted date string
|
|
102
|
+
*/
|
|
103
|
+
export function formatDate(date: Date, locale = 'en-US'): string {
|
|
104
|
+
return date.toLocaleDateString(locale);
|
|
105
|
+
}
|
|
106
|
+
```
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: WordPress plugin architecture standards for Silver Assist plugins including LoadableInterface, PSR-4, MVC pattern, singleton, and SilverAssist packages
|
|
3
|
+
name: WordPress Plugin Architecture
|
|
4
|
+
applyTo: "**/*.php"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# WordPress Plugin Architecture — Silver Assist Plugins
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Core Standards
|
|
12
|
+
|
|
13
|
+
| Attribute | Value |
|
|
14
|
+
|-----------|-------|
|
|
15
|
+
| PHP | 8.2+ |
|
|
16
|
+
| WordPress | 6.5+ |
|
|
17
|
+
| Autoloading | PSR-4 (PascalCase files and directories) |
|
|
18
|
+
| Standards | PHPCS (WordPress-Extra), PHPStan Level 8 |
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## PSR-4 Autoloading
|
|
23
|
+
|
|
24
|
+
Namespace `SilverAssist\PluginName\` maps to `includes/`:
|
|
25
|
+
|
|
26
|
+
- PHP classes: `PascalCase.php` matching class name exactly
|
|
27
|
+
- Directories: `PascalCase/` (e.g., `Core/`, `Admin/`, `Service/`)
|
|
28
|
+
- Assets: `kebab-case.css`, `kebab-case.js`
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## LoadableInterface Priority System
|
|
33
|
+
|
|
34
|
+
All components implement `LoadableInterface` with three methods:
|
|
35
|
+
|
|
36
|
+
```php
|
|
37
|
+
interface LoadableInterface {
|
|
38
|
+
public function init(): void;
|
|
39
|
+
public function get_priority(): int;
|
|
40
|
+
public function should_load(): bool;
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Priority values:
|
|
45
|
+
- **10**: Core components (Plugin, Activator, critical services)
|
|
46
|
+
- **20**: Services (business logic, API clients)
|
|
47
|
+
- **30**: Admin components (controllers, settings pages)
|
|
48
|
+
- **40**: Utils & Assets (helpers, loggers)
|
|
49
|
+
|
|
50
|
+
### Singleton Pattern
|
|
51
|
+
|
|
52
|
+
```php
|
|
53
|
+
class ServiceName implements LoadableInterface {
|
|
54
|
+
private static ?self $instance = null;
|
|
55
|
+
|
|
56
|
+
public static function instance(): self {
|
|
57
|
+
if ( null === self::$instance ) {
|
|
58
|
+
self::$instance = new self();
|
|
59
|
+
}
|
|
60
|
+
return self::$instance;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private function __construct() {}
|
|
64
|
+
|
|
65
|
+
public function init(): void {
|
|
66
|
+
// Register hooks.
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public function get_priority(): int {
|
|
70
|
+
return 20;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public function should_load(): bool {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Component Loading (Plugin.php)
|
|
80
|
+
|
|
81
|
+
```php
|
|
82
|
+
private function get_components(): array {
|
|
83
|
+
return [
|
|
84
|
+
// Core — Priority 10.
|
|
85
|
+
\SilverAssist\PluginName\Core\Activator::class,
|
|
86
|
+
// Services — Priority 20.
|
|
87
|
+
\SilverAssist\PluginName\Service\ServiceName::class,
|
|
88
|
+
// Controllers — Priority 30.
|
|
89
|
+
\SilverAssist\PluginName\Controller\Admin\PageController::class,
|
|
90
|
+
// Utils — Priority 40.
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private function load_components(): void {
|
|
95
|
+
$components = [];
|
|
96
|
+
foreach ( $this->get_components() as $class ) {
|
|
97
|
+
if ( method_exists( $class, 'instance' ) ) {
|
|
98
|
+
$instance = $class::instance();
|
|
99
|
+
if ( $instance->should_load() ) {
|
|
100
|
+
$components[] = $instance;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
usort( $components, fn( $a, $b ) => $a->get_priority() <=> $b->get_priority() );
|
|
105
|
+
foreach ( $components as $component ) {
|
|
106
|
+
$component->init();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## MVC Flow Pattern
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
User Request → Controller → Service → Repository/WordPress API
|
|
117
|
+
↓
|
|
118
|
+
View::render($data) ← Static call with prepared data
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
| Layer | Responsibility | Can Depend On |
|
|
122
|
+
|-------|---------------|---------------|
|
|
123
|
+
| **Controller** | Handle requests, prepare data, coordinate | Services, Views |
|
|
124
|
+
| **Service** | Business logic, data processing | Other Services, Repositories |
|
|
125
|
+
| **View** | Render HTML (static class) | Only data passed as parameters |
|
|
126
|
+
| **Model** | Data structures | Nothing |
|
|
127
|
+
|
|
128
|
+
### CRITICAL: Views MUST NOT Instantiate Services
|
|
129
|
+
|
|
130
|
+
```php
|
|
131
|
+
// ✅ CORRECT — Controller prepares data, View receives it.
|
|
132
|
+
class PageController {
|
|
133
|
+
public function render_page(): void {
|
|
134
|
+
$data = $this->service->get_data();
|
|
135
|
+
PageView::render( $data );
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
class PageView {
|
|
140
|
+
public static function render( array $data ): void {
|
|
141
|
+
// Only use data passed as parameters.
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ❌ WRONG — View instantiates Service.
|
|
146
|
+
class PageView {
|
|
147
|
+
public static function render(): void {
|
|
148
|
+
$service = ServiceName::instance(); // ❌ Never do this.
|
|
149
|
+
$data = $service->get_data();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Directory Structure
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
plugin-name/
|
|
160
|
+
├── plugin-name.php # Main plugin file
|
|
161
|
+
├── composer.json
|
|
162
|
+
├── phpcs.xml / phpstan.neon / phpunit.xml.dist
|
|
163
|
+
├── includes/ # PSR-4 classes
|
|
164
|
+
│ ├── Core/ # Priority 10
|
|
165
|
+
│ │ ├── Plugin.php
|
|
166
|
+
│ │ ├── Activator.php
|
|
167
|
+
│ │ └── Interfaces/LoadableInterface.php
|
|
168
|
+
│ ├── Service/ # Priority 20
|
|
169
|
+
│ ├── Controller/ # Priority 30
|
|
170
|
+
│ ├── View/ # Static HTML rendering
|
|
171
|
+
│ ├── Model/ # Domain models
|
|
172
|
+
│ ├── Repository/ # Data access
|
|
173
|
+
│ ├── Infrastructure/ # WordPress integrations
|
|
174
|
+
│ └── Utils/ # Priority 40
|
|
175
|
+
├── assets/ (css/, js/)
|
|
176
|
+
├── languages/
|
|
177
|
+
├── scripts/
|
|
178
|
+
├── tests/ (Unit/, Integration/, Helpers/)
|
|
179
|
+
└── .github/ (workflows/, copilot-instructions.md)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## SilverAssist Packages
|
|
185
|
+
|
|
186
|
+
### wp-github-updater
|
|
187
|
+
|
|
188
|
+
Enables automatic updates from GitHub releases:
|
|
189
|
+
|
|
190
|
+
```php
|
|
191
|
+
$config = new \SilverAssist\GitHubUpdater\UpdaterConfig(
|
|
192
|
+
plugin_basename: PLUGIN_BASENAME,
|
|
193
|
+
version: PLUGIN_VERSION,
|
|
194
|
+
github_repo: 'SilverAssist/plugin-slug',
|
|
195
|
+
plugin_file: PLUGIN_FILE
|
|
196
|
+
);
|
|
197
|
+
$config->init();
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### wp-settings-hub
|
|
201
|
+
|
|
202
|
+
Shared admin dashboard and settings infrastructure.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Security Guards
|
|
207
|
+
|
|
208
|
+
### File Header
|
|
209
|
+
|
|
210
|
+
Every PHP file must start with:
|
|
211
|
+
|
|
212
|
+
```php
|
|
213
|
+
\defined( 'ABSPATH' ) || exit;
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Composer Autoloader Validation
|
|
217
|
+
|
|
218
|
+
Main plugin file validates autoloader path to prevent path traversal:
|
|
219
|
+
|
|
220
|
+
```php
|
|
221
|
+
$autoload_path = PLUGIN_PATH . 'vendor/autoload.php';
|
|
222
|
+
$real_autoload = realpath( $autoload_path );
|
|
223
|
+
$real_plugin = realpath( PLUGIN_PATH );
|
|
224
|
+
|
|
225
|
+
if ( $real_autoload && $real_plugin && 0 === strpos( $real_autoload, $real_plugin ) ) {
|
|
226
|
+
require_once $real_autoload;
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Plugin Header — Update URI
|
|
231
|
+
|
|
232
|
+
**CRITICAL**: Must include `Update URI` header to prevent WordPress.org update conflicts:
|
|
233
|
+
|
|
234
|
+
```php
|
|
235
|
+
/**
|
|
236
|
+
* Update URI: https://github.com/SilverAssist/plugin-slug
|
|
237
|
+
*/
|
|
238
|
+
```
|