ima-claude 2.9.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 +21 -0
- package/README.md +463 -0
- package/dist/cli.js +1064 -0
- package/package.json +49 -0
- package/platforms/claude/adapter.ts +115 -0
- package/platforms/junie/adapter.ts +254 -0
- package/platforms/junie/agents-template.md +113 -0
- package/platforms/junie/hook-translations.md +84 -0
- package/platforms/shared/detector.ts +27 -0
- package/platforms/shared/installer.ts +202 -0
- package/platforms/shared/types.ts +78 -0
- package/plugins/ima-claude/.claude-plugin/plugin.json +25 -0
- package/plugins/ima-claude/agents/explorer.md +30 -0
- package/plugins/ima-claude/agents/implementer.md +30 -0
- package/plugins/ima-claude/agents/memory.md +42 -0
- package/plugins/ima-claude/agents/reviewer.md +53 -0
- package/plugins/ima-claude/agents/tester.md +33 -0
- package/plugins/ima-claude/agents/wp-developer.md +46 -0
- package/plugins/ima-claude/hooks/README.md +145 -0
- package/plugins/ima-claude/hooks/atlassian_prereqs.py +112 -0
- package/plugins/ima-claude/hooks/block_sed_edits.py +59 -0
- package/plugins/ima-claude/hooks/bootstrap.sh +90 -0
- package/plugins/ima-claude/hooks/bootstrap_utility_check.py +94 -0
- package/plugins/ima-claude/hooks/composer_autoload_check.py +70 -0
- package/plugins/ima-claude/hooks/docs_organization.py +104 -0
- package/plugins/ima-claude/hooks/enforce_rg_over_grep.py +56 -0
- package/plugins/ima-claude/hooks/fp_utility_check.py +90 -0
- package/plugins/ima-claude/hooks/hook_logger.py +69 -0
- package/plugins/ima-claude/hooks/hooks.json +239 -0
- package/plugins/ima-claude/hooks/jira_issue_fetch.py +79 -0
- package/plugins/ima-claude/hooks/jquery_in_wordpress.py +92 -0
- package/plugins/ima-claude/hooks/memory_bootstrap.py +79 -0
- package/plugins/ima-claude/hooks/memory_store_reminder.py +75 -0
- package/plugins/ima-claude/hooks/prompt_coach.py +125 -0
- package/plugins/ima-claude/hooks/prompt_coach_digest.md +48 -0
- package/plugins/ima-claude/hooks/prompt_coach_system.md +30 -0
- package/plugins/ima-claude/hooks/sequential_thinking_check.py +81 -0
- package/plugins/ima-claude/hooks/serena_over_grep.py +96 -0
- package/plugins/ima-claude/hooks/serena_over_read.py +66 -0
- package/plugins/ima-claude/hooks/serena_project_check.py +133 -0
- package/plugins/ima-claude/hooks/sql_injection_check.py +73 -0
- package/plugins/ima-claude/hooks/task_master_after_plan.py +31 -0
- package/plugins/ima-claude/hooks/task_master_before_impl.py +93 -0
- package/plugins/ima-claude/hooks/tavily_extract_advanced.py +48 -0
- package/plugins/ima-claude/hooks/vestige_before_external.py +86 -0
- package/plugins/ima-claude/hooks/webfetch_to_tavily.py +42 -0
- package/plugins/ima-claude/hooks/websearch_to_tavily.py +41 -0
- package/plugins/ima-claude/hooks/wp_security_check.py +150 -0
- package/plugins/ima-claude/personalities/README.md +45 -0
- package/plugins/ima-claude/personalities/enable-40k.md +69 -0
- package/plugins/ima-claude/personalities/enable-templars.md +69 -0
- package/plugins/ima-claude/skills/.research-summary.md +340 -0
- package/plugins/ima-claude/skills/architect/SKILL.md +304 -0
- package/plugins/ima-claude/skills/compound-bridge/SKILL.md +200 -0
- package/plugins/ima-claude/skills/discourse/SKILL.md +440 -0
- package/plugins/ima-claude/skills/discourse-admin/SKILL.md +192 -0
- package/plugins/ima-claude/skills/discourse-admin/references/api-endpoints.md +441 -0
- package/plugins/ima-claude/skills/discourse-admin/references/gotchas.md +107 -0
- package/plugins/ima-claude/skills/discourse-admin/references/staging-defaults.md +98 -0
- package/plugins/ima-claude/skills/discourse-admin/scripts/discourse-admin.py +319 -0
- package/plugins/ima-claude/skills/docs-organize/SKILL.md +254 -0
- package/plugins/ima-claude/skills/docs-organize/templates/active-README.md +50 -0
- package/plugins/ima-claude/skills/docs-organize/templates/archive-README.md +57 -0
- package/plugins/ima-claude/skills/docs-organize/templates/docs-README.md +43 -0
- package/plugins/ima-claude/skills/docs-organize/templates/phase-archive-README.md +83 -0
- package/plugins/ima-claude/skills/docs-organize/templates/section-README.md +48 -0
- package/plugins/ima-claude/skills/docs-organize/templates/transient-README.md +79 -0
- package/plugins/ima-claude/skills/docs-organize/templates/transient-gitignore +9 -0
- package/plugins/ima-claude/skills/ember-discourse/SKILL.md +496 -0
- package/plugins/ima-claude/skills/functional-programmer/SKILL.md +258 -0
- package/plugins/ima-claude/skills/ima-bootstrap/SKILL.md +278 -0
- package/plugins/ima-claude/skills/ima-bootstrap/references/bootstrap-patterns.md +356 -0
- package/plugins/ima-claude/skills/ima-bootstrap/references/ima-brand.md +273 -0
- package/plugins/ima-claude/skills/ima-bootstrap/references/theme-integration.md +212 -0
- package/plugins/ima-claude/skills/ima-brand/SKILL.md +108 -0
- package/plugins/ima-claude/skills/ima-brand/references/brand-identity.md +140 -0
- package/plugins/ima-claude/skills/ima-brand/references/digital-standards.md +180 -0
- package/plugins/ima-claude/skills/ima-brand/references/visual-system.md +173 -0
- package/plugins/ima-claude/skills/ima-forms-expert/SKILL.md +175 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/container-components.md +154 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/examples.md +328 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/field-components.md +298 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/form-factory.md +193 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/quick-reference.md +153 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/validation-engine.md +336 -0
- package/plugins/ima-claude/skills/jira-checkpoint/SKILL.md +178 -0
- package/plugins/ima-claude/skills/jquery/SKILL.md +413 -0
- package/plugins/ima-claude/skills/js-fp/SKILL.md +463 -0
- package/plugins/ima-claude/skills/js-fp/core-principles.md +487 -0
- package/plugins/ima-claude/skills/js-fp/examples/pure-functions.js +260 -0
- package/plugins/ima-claude/skills/js-fp/examples/tests/pure-functions.test.js +262 -0
- package/plugins/ima-claude/skills/js-fp/references/anti-patterns.md +120 -0
- package/plugins/ima-claude/skills/js-fp/references/performance-patterns.md +116 -0
- package/plugins/ima-claude/skills/js-fp/references/testing-patterns.md +134 -0
- package/plugins/ima-claude/skills/js-fp-api/SKILL.md +280 -0
- package/plugins/ima-claude/skills/js-fp-api/examples/crud-endpoint.js +258 -0
- package/plugins/ima-claude/skills/js-fp-api/references/middleware-patterns.md +134 -0
- package/plugins/ima-claude/skills/js-fp-api/references/security-sql.md +110 -0
- package/plugins/ima-claude/skills/js-fp-api/references/validation-patterns.md +165 -0
- package/plugins/ima-claude/skills/js-fp-react/SKILL.md +447 -0
- package/plugins/ima-claude/skills/js-fp-react/examples/ProductCard.tsx +65 -0
- package/plugins/ima-claude/skills/js-fp-react/references/hooks-advanced.md +136 -0
- package/plugins/ima-claude/skills/js-fp-react/references/performance-patterns.md +175 -0
- package/plugins/ima-claude/skills/js-fp-vue/SKILL.md +322 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/complete-examples.md +397 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/composables-advanced.md +282 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/reactivity-patterns.md +348 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/testing.md +314 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/SKILL.md +301 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/references/ajax-patterns.md +192 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/references/event-patterns.md +136 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/references/wp-integration.md +248 -0
- package/plugins/ima-claude/skills/livecanvas/SKILL.md +209 -0
- package/plugins/ima-claude/skills/livecanvas/references/livecanvas-features.md +311 -0
- package/plugins/ima-claude/skills/livecanvas/references/loops-and-logic.md +730 -0
- package/plugins/ima-claude/skills/livecanvas/references/picostrap.md +227 -0
- package/plugins/ima-claude/skills/mcp-atlassian/SKILL.md +339 -0
- package/plugins/ima-claude/skills/mcp-context7/SKILL.md +109 -0
- package/plugins/ima-claude/skills/mcp-memory/SKILL.md +182 -0
- package/plugins/ima-claude/skills/mcp-qdrant/SKILL.md +233 -0
- package/plugins/ima-claude/skills/mcp-sequential/SKILL.md +149 -0
- package/plugins/ima-claude/skills/mcp-serena/SKILL.md +174 -0
- package/plugins/ima-claude/skills/mcp-tavily/SKILL.md +118 -0
- package/plugins/ima-claude/skills/mcp-vestige/SKILL.md +259 -0
- package/plugins/ima-claude/skills/php-authnet/SKILL.md +275 -0
- package/plugins/ima-claude/skills/php-authnet/references/api-reference.md +624 -0
- package/plugins/ima-claude/skills/php-authnet/references/sandbox-testing.md +424 -0
- package/plugins/ima-claude/skills/php-fp/SKILL.md +333 -0
- package/plugins/ima-claude/skills/php-fp/examples/pure-functions.php +403 -0
- package/plugins/ima-claude/skills/php-fp/examples/tests/PureFunctionsTest.php +515 -0
- package/plugins/ima-claude/skills/php-fp/references/core-principles.md +277 -0
- package/plugins/ima-claude/skills/php-fp/references/testing-patterns.md +374 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/SKILL.md +216 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/fp-patterns.md +275 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/plugin-architecture.md +295 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/security-examples.md +203 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/testing-strategy.md +259 -0
- package/plugins/ima-claude/skills/phpunit-wp/SKILL.md +716 -0
- package/plugins/ima-claude/skills/playwright/SKILL.md +434 -0
- package/plugins/ima-claude/skills/playwright/references/accessibility-testing.md +153 -0
- package/plugins/ima-claude/skills/playwright/references/ci-cd.md +268 -0
- package/plugins/ima-claude/skills/playwright/references/network-mocking.md +270 -0
- package/plugins/ima-claude/skills/playwright/references/visual-regression.md +215 -0
- package/plugins/ima-claude/skills/py-fp/SKILL.md +663 -0
- package/plugins/ima-claude/skills/py-fp/examples/pure-functions.py +185 -0
- package/plugins/ima-claude/skills/py-fp/examples/tests/test_pure_functions.py +244 -0
- package/plugins/ima-claude/skills/py-fp/references/core-principles.md +381 -0
- package/plugins/ima-claude/skills/py-fp/references/testing-patterns.md +283 -0
- package/plugins/ima-claude/skills/quasar-fp/SKILL.md +327 -0
- package/plugins/ima-claude/skills/quasar-fp/metadata.json +85 -0
- package/plugins/ima-claude/skills/quasar-fp/references/component-patterns.md +257 -0
- package/plugins/ima-claude/skills/quasar-fp/references/theme-integration.md +233 -0
- package/plugins/ima-claude/skills/quasar-fp/references/utility-classes.md +237 -0
- package/plugins/ima-claude/skills/quickstart/SKILL.md +129 -0
- package/plugins/ima-claude/skills/rails/SKILL.md +359 -0
- package/plugins/ima-claude/skills/resume-session/SKILL.md +68 -0
- package/plugins/ima-claude/skills/rg/SKILL.md +205 -0
- package/plugins/ima-claude/skills/ruby-fp/SKILL.md +336 -0
- package/plugins/ima-claude/skills/save-session/SKILL.md +81 -0
- package/plugins/ima-claude/skills/scorecard/SKILL.md +96 -0
- package/plugins/ima-claude/skills/skill-analyzer/SKILL.md +127 -0
- package/plugins/ima-claude/skills/skill-analyzer/references/advanced-checklist.md +44 -0
- package/plugins/ima-claude/skills/skill-analyzer/references/core-checklist.md +60 -0
- package/plugins/ima-claude/skills/skill-analyzer/scripts/analyze_skill.py +418 -0
- package/plugins/ima-claude/skills/skill-creator/LICENSE.txt +202 -0
- package/plugins/ima-claude/skills/skill-creator/SKILL.md +343 -0
- package/plugins/ima-claude/skills/skill-creator/references/output-patterns.md +82 -0
- package/plugins/ima-claude/skills/skill-creator/references/workflows.md +28 -0
- package/plugins/ima-claude/skills/skill-creator/scripts/init_skill.py +303 -0
- package/plugins/ima-claude/skills/skill-creator/scripts/package_skill.py +110 -0
- package/plugins/ima-claude/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/plugins/ima-claude/skills/task-master/SKILL.md +51 -0
- package/plugins/ima-claude/skills/task-planner/SKILL.md +228 -0
- package/plugins/ima-claude/skills/task-runner/SKILL.md +192 -0
- package/plugins/ima-claude/skills/unit-testing/SKILL.md +198 -0
- package/plugins/ima-claude/skills/unit-testing/references/mock-patterns.md +181 -0
- package/plugins/ima-claude/skills/unit-testing/references/tdd-workflow.md +177 -0
- package/plugins/ima-claude/skills/unit-testing/references/test-strategy.md +126 -0
- package/plugins/ima-claude/skills/wp-local/SKILL.md +246 -0
- package/plugins/ima-claude/skills/wp-local/references/configuration.md +198 -0
- package/plugins/ima-claude/skills/wp-local/references/wp-cli-reference.md +406 -0
- package/plugins/ima-claude/skills/wp-local/scripts/wp-local.sh +61 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# PHP FP Core Principles
|
|
2
|
+
|
|
3
|
+
Deep-dive into functional programming philosophy adapted for PHP 8.1+.
|
|
4
|
+
|
|
5
|
+
## The Three Laws
|
|
6
|
+
|
|
7
|
+
### 1. Purity: Same Input, Same Output
|
|
8
|
+
|
|
9
|
+
```php
|
|
10
|
+
<?php
|
|
11
|
+
declare(strict_types=1);
|
|
12
|
+
|
|
13
|
+
// PURE: Deterministic, no side effects
|
|
14
|
+
function calculateTax(float $amount, float $rate): float {
|
|
15
|
+
return $amount * $rate;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// IMPURE: Uses external state
|
|
19
|
+
function calculateTax(float $amount): float {
|
|
20
|
+
global $taxRate; // Hidden dependency
|
|
21
|
+
file_put_contents('/tmp/log', $amount); // Side effect
|
|
22
|
+
return $amount * $taxRate;
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Test**: Can you call this function 1000 times with the same input and always get the same result without affecting anything else?
|
|
27
|
+
|
|
28
|
+
### 2. Immutability: Never Mutate, Always Return New
|
|
29
|
+
|
|
30
|
+
```php
|
|
31
|
+
<?php
|
|
32
|
+
declare(strict_types=1);
|
|
33
|
+
|
|
34
|
+
// MUTATES: Modifies input
|
|
35
|
+
function addTimestamp(array &$record): void {
|
|
36
|
+
$record['created_at'] = time();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// IMMUTABLE: Returns new array
|
|
40
|
+
function addTimestamp(array $record): array {
|
|
41
|
+
return [...$record, 'created_at' => time()];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// IMMUTABLE: Collection operations
|
|
45
|
+
function removeInactive(array $users): array {
|
|
46
|
+
return array_values(array_filter($users, fn($u) => $u['active']));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function updateById(array $items, int $id, array $updates): array {
|
|
50
|
+
return array_map(
|
|
51
|
+
fn($item) => $item['id'] === $id ? [...$item, ...$updates] : $item,
|
|
52
|
+
$items
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 3. Composition: Small Functions, Combined Simply
|
|
58
|
+
|
|
59
|
+
```php
|
|
60
|
+
<?php
|
|
61
|
+
declare(strict_types=1);
|
|
62
|
+
|
|
63
|
+
// Build complex from simple - NO pipe() utility needed
|
|
64
|
+
function processOrder(array $order): array {
|
|
65
|
+
$validated = validateOrder($order);
|
|
66
|
+
if (!$validated['success']) return $validated;
|
|
67
|
+
|
|
68
|
+
$calculated = calculateTotals($validated['data']);
|
|
69
|
+
if (!$calculated['success']) return $calculated;
|
|
70
|
+
|
|
71
|
+
return applyDiscounts($calculated['data']);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Each function is small, pure, testable
|
|
75
|
+
function validateOrder(array $order): array {
|
|
76
|
+
if (empty($order['items'])) {
|
|
77
|
+
return ['success' => false, 'error' => 'No items'];
|
|
78
|
+
}
|
|
79
|
+
return ['success' => true, 'data' => $order];
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## PHP-Native Patterns
|
|
84
|
+
|
|
85
|
+
### Closures as First-Class Functions
|
|
86
|
+
|
|
87
|
+
```php
|
|
88
|
+
<?php
|
|
89
|
+
declare(strict_types=1);
|
|
90
|
+
|
|
91
|
+
// Factory returns configured closure
|
|
92
|
+
function createValidator(int $minLength): Closure {
|
|
93
|
+
return fn(string $value): bool => strlen($value) >= $minLength;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
$validateName = createValidator(2);
|
|
97
|
+
$validateName('Jo'); // true
|
|
98
|
+
|
|
99
|
+
// Compose validators
|
|
100
|
+
function validateAll(array $validators): Closure {
|
|
101
|
+
return fn($value): bool => array_reduce(
|
|
102
|
+
$validators,
|
|
103
|
+
fn(bool $valid, callable $v) => $valid && $v($value),
|
|
104
|
+
true
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
$validateUsername = validateAll([
|
|
109
|
+
fn($v) => is_string($v),
|
|
110
|
+
fn($v) => strlen($v) >= 3,
|
|
111
|
+
fn($v) => strlen($v) <= 20,
|
|
112
|
+
fn($v) => preg_match('/^[a-z0-9_]+$/', $v) === 1,
|
|
113
|
+
]);
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Match Expressions for Branching
|
|
117
|
+
|
|
118
|
+
```php
|
|
119
|
+
<?php
|
|
120
|
+
declare(strict_types=1);
|
|
121
|
+
|
|
122
|
+
// Pure branching with match
|
|
123
|
+
function getStatusMessage(string $status): string {
|
|
124
|
+
return match($status) {
|
|
125
|
+
'pending' => 'Awaiting review',
|
|
126
|
+
'approved' => 'Ready to proceed',
|
|
127
|
+
'rejected' => 'Request denied',
|
|
128
|
+
'cancelled' => 'User cancelled',
|
|
129
|
+
default => 'Unknown status',
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Type-safe dispatch
|
|
134
|
+
function processEvent(array $event): array {
|
|
135
|
+
return match($event['type']) {
|
|
136
|
+
'user.created' => handleUserCreated($event['data']),
|
|
137
|
+
'user.updated' => handleUserUpdated($event['data']),
|
|
138
|
+
'user.deleted' => handleUserDeleted($event['data']),
|
|
139
|
+
default => ['handled' => false, 'reason' => 'Unknown event'],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Readonly Properties for Immutable Objects
|
|
145
|
+
|
|
146
|
+
```php
|
|
147
|
+
<?php
|
|
148
|
+
declare(strict_types=1);
|
|
149
|
+
|
|
150
|
+
// Value objects with readonly (PHP 8.1+)
|
|
151
|
+
readonly class Money {
|
|
152
|
+
public function __construct(
|
|
153
|
+
public int $amount,
|
|
154
|
+
public string $currency
|
|
155
|
+
) {}
|
|
156
|
+
|
|
157
|
+
public function add(Money $other): Money {
|
|
158
|
+
if ($this->currency !== $other->currency) {
|
|
159
|
+
throw new InvalidArgumentException('Currency mismatch');
|
|
160
|
+
}
|
|
161
|
+
return new Money($this->amount + $other->amount, $this->currency);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Usage - immutable by design
|
|
166
|
+
$price = new Money(1000, 'USD');
|
|
167
|
+
$tax = new Money(100, 'USD');
|
|
168
|
+
$total = $price->add($tax); // New Money object
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Anti-Pattern Recognition
|
|
172
|
+
|
|
173
|
+
### Hidden Mutation
|
|
174
|
+
|
|
175
|
+
```php
|
|
176
|
+
<?php
|
|
177
|
+
// ANTI-PATTERN: Mutation disguised as transformation
|
|
178
|
+
function processUser(array $user): array {
|
|
179
|
+
$user['processed_at'] = time(); // Mutates if passed by reference
|
|
180
|
+
return validateUser($user);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// CORRECT: Explicit new array
|
|
184
|
+
function processUser(array $user): array {
|
|
185
|
+
return validateUser([
|
|
186
|
+
...$user,
|
|
187
|
+
'processed_at' => time()
|
|
188
|
+
]);
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Configuration in Hot Paths
|
|
193
|
+
|
|
194
|
+
```php
|
|
195
|
+
<?php
|
|
196
|
+
// ANTI-PATTERN: O(records x fields) - schema accessed every iteration
|
|
197
|
+
function mapRecords(array $records, array $schema): array {
|
|
198
|
+
return array_map(function($record) use ($schema) {
|
|
199
|
+
return array_combine(
|
|
200
|
+
array_column($schema['fields'], 'name'),
|
|
201
|
+
array_map(fn($f) => $record[$f['key']], $schema['fields'])
|
|
202
|
+
);
|
|
203
|
+
}, $records);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// CORRECT: Pre-compile, then execute - O(records + fields)
|
|
207
|
+
function createRecordMapper(array $schema): Closure {
|
|
208
|
+
$fields = $schema['fields'];
|
|
209
|
+
$names = array_column($fields, 'name');
|
|
210
|
+
$keys = array_column($fields, 'key');
|
|
211
|
+
|
|
212
|
+
return fn(array $record): array => array_combine(
|
|
213
|
+
$names,
|
|
214
|
+
array_map(fn($key) => $record[$key], $keys)
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
$mapper = createRecordMapper($schema); // Setup once
|
|
219
|
+
$results = array_map($mapper, $records); // Linear execution
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Over-Engineering Validation
|
|
223
|
+
|
|
224
|
+
```php
|
|
225
|
+
<?php
|
|
226
|
+
// ANTI-PATTERN: Complex machinery for simple check
|
|
227
|
+
$emailValidator = (new ValidatorBuilder())
|
|
228
|
+
->addRule(new FormatRule(new EmailPattern()))
|
|
229
|
+
->addRule(new LengthRule(new Range(5, 254)))
|
|
230
|
+
->addTransformer(new LowercaseTransformer())
|
|
231
|
+
->build();
|
|
232
|
+
|
|
233
|
+
// CORRECT: Simple, direct, readable
|
|
234
|
+
function validateEmail(string $email): bool {
|
|
235
|
+
$normalized = strtolower(trim($email));
|
|
236
|
+
return strlen($normalized) >= 5
|
|
237
|
+
&& strlen($normalized) <= 254
|
|
238
|
+
&& filter_var($normalized, FILTER_VALIDATE_EMAIL) !== false;
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Result Type Pattern
|
|
243
|
+
|
|
244
|
+
Consistent error handling without exceptions for expected failures.
|
|
245
|
+
|
|
246
|
+
```php
|
|
247
|
+
<?php
|
|
248
|
+
declare(strict_types=1);
|
|
249
|
+
|
|
250
|
+
// Standard result shape
|
|
251
|
+
function success(mixed $data): array {
|
|
252
|
+
return ['success' => true, 'data' => $data];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function failure(string $error, array $context = []): array {
|
|
256
|
+
return ['success' => false, 'error' => $error, 'context' => $context];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Usage in pure functions
|
|
260
|
+
function parseJson(string $json): array {
|
|
261
|
+
$data = json_decode($json, true);
|
|
262
|
+
return json_last_error() === JSON_ERROR_NONE
|
|
263
|
+
? success($data)
|
|
264
|
+
: failure('Invalid JSON', ['error' => json_last_error_msg()]);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Chain results
|
|
268
|
+
function processInput(string $json): array {
|
|
269
|
+
$parsed = parseJson($json);
|
|
270
|
+
if (!$parsed['success']) return $parsed;
|
|
271
|
+
|
|
272
|
+
$validated = validateData($parsed['data']);
|
|
273
|
+
if (!$validated['success']) return $validated;
|
|
274
|
+
|
|
275
|
+
return transformData($validated['data']);
|
|
276
|
+
}
|
|
277
|
+
```
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
# PHP FP Testing Patterns
|
|
2
|
+
|
|
3
|
+
Comprehensive testing philosophy for pure functions using PHPUnit.
|
|
4
|
+
|
|
5
|
+
## Why Pure Functions Enable Better Testing
|
|
6
|
+
|
|
7
|
+
Pure functions guarantee:
|
|
8
|
+
- **Deterministic**: Same input always produces same output
|
|
9
|
+
- **Isolated**: No side effects means tests never interfere
|
|
10
|
+
- **Fast**: No I/O, network, or database needed
|
|
11
|
+
- **Complete**: All edge cases can be systematically tested
|
|
12
|
+
|
|
13
|
+
## The Test Matrix Approach
|
|
14
|
+
|
|
15
|
+
### Systematic Type Coverage
|
|
16
|
+
|
|
17
|
+
```php
|
|
18
|
+
<?php
|
|
19
|
+
declare(strict_types=1);
|
|
20
|
+
|
|
21
|
+
use PHPUnit\Framework\TestCase;
|
|
22
|
+
use PHPUnit\Framework\Attributes\DataProvider;
|
|
23
|
+
|
|
24
|
+
class ValidatorTest extends TestCase
|
|
25
|
+
{
|
|
26
|
+
// Test expected use cases
|
|
27
|
+
#[DataProvider('validInputProvider')]
|
|
28
|
+
public function testAcceptsValidInput(mixed $input, bool $expected): void
|
|
29
|
+
{
|
|
30
|
+
$this->assertSame($expected, validatePositiveInteger($input));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public static function validInputProvider(): array
|
|
34
|
+
{
|
|
35
|
+
return [
|
|
36
|
+
'positive integer' => [5, true],
|
|
37
|
+
'large number' => [1000000, true],
|
|
38
|
+
'boundary: 1' => [1, true],
|
|
39
|
+
'zero' => [0, false],
|
|
40
|
+
'negative' => [-5, false],
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Test ALL PHP types systematically
|
|
45
|
+
#[DataProvider('invalidTypeProvider')]
|
|
46
|
+
public function testRejectsInvalidTypes(mixed $input): void
|
|
47
|
+
{
|
|
48
|
+
$this->expectException(TypeError::class);
|
|
49
|
+
validatePositiveInteger($input);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public static function invalidTypeProvider(): array
|
|
53
|
+
{
|
|
54
|
+
return [
|
|
55
|
+
'null' => [null],
|
|
56
|
+
'string' => ['5'],
|
|
57
|
+
'float' => [5.0],
|
|
58
|
+
'bool true' => [true],
|
|
59
|
+
'bool false' => [false],
|
|
60
|
+
'empty array' => [[]],
|
|
61
|
+
'array with values' => [[1, 2, 3]],
|
|
62
|
+
'object' => [new stdClass()],
|
|
63
|
+
'closure' => [fn() => 5],
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Boundary Value Testing
|
|
70
|
+
|
|
71
|
+
```php
|
|
72
|
+
<?php
|
|
73
|
+
declare(strict_types=1);
|
|
74
|
+
|
|
75
|
+
class RangeValidatorTest extends TestCase
|
|
76
|
+
{
|
|
77
|
+
private Closure $validator;
|
|
78
|
+
|
|
79
|
+
protected function setUp(): void
|
|
80
|
+
{
|
|
81
|
+
$this->validator = createRangeValidator(min: 0, max: 100);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#[DataProvider('boundaryProvider')]
|
|
85
|
+
public function testBoundaryValues(int $value, bool $expected): void
|
|
86
|
+
{
|
|
87
|
+
$this->assertSame($expected, ($this->validator)($value));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public static function boundaryProvider(): array
|
|
91
|
+
{
|
|
92
|
+
return [
|
|
93
|
+
'below min' => [-1, false],
|
|
94
|
+
'at min' => [0, true],
|
|
95
|
+
'above min' => [1, true],
|
|
96
|
+
'middle' => [50, true],
|
|
97
|
+
'below max' => [99, true],
|
|
98
|
+
'at max' => [100, true],
|
|
99
|
+
'above max' => [101, false],
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Testing Composed Functions
|
|
106
|
+
|
|
107
|
+
### Test Components Independently
|
|
108
|
+
|
|
109
|
+
```php
|
|
110
|
+
<?php
|
|
111
|
+
declare(strict_types=1);
|
|
112
|
+
|
|
113
|
+
// Functions under test
|
|
114
|
+
function normalizeEmail(string $email): string {
|
|
115
|
+
return strtolower(trim($email));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function validateEmailFormat(string $email): bool {
|
|
119
|
+
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function validateEmailDomain(string $email, array $blocked): bool {
|
|
123
|
+
$domain = substr($email, strpos($email, '@') + 1);
|
|
124
|
+
return !in_array($domain, $blocked, true);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Test each component
|
|
128
|
+
class EmailValidationTest extends TestCase
|
|
129
|
+
{
|
|
130
|
+
public function testNormalizeEmail(): void
|
|
131
|
+
{
|
|
132
|
+
$this->assertSame('test@example.com', normalizeEmail(' TEST@EXAMPLE.COM '));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
#[DataProvider('formatProvider')]
|
|
136
|
+
public function testValidateEmailFormat(string $email, bool $valid): void
|
|
137
|
+
{
|
|
138
|
+
$this->assertSame($valid, validateEmailFormat($email));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
public static function formatProvider(): array
|
|
142
|
+
{
|
|
143
|
+
return [
|
|
144
|
+
'valid' => ['test@example.com', true],
|
|
145
|
+
'no @' => ['testexample.com', false],
|
|
146
|
+
'no domain' => ['test@', false],
|
|
147
|
+
'no local' => ['@example.com', false],
|
|
148
|
+
'double @' => ['test@@example.com', false],
|
|
149
|
+
];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
public function testValidateEmailDomain(): void
|
|
153
|
+
{
|
|
154
|
+
$blocked = ['spam.com', 'temp.net'];
|
|
155
|
+
|
|
156
|
+
$this->assertTrue(validateEmailDomain('user@example.com', $blocked));
|
|
157
|
+
$this->assertFalse(validateEmailDomain('user@spam.com', $blocked));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Test the Composition
|
|
163
|
+
|
|
164
|
+
```php
|
|
165
|
+
<?php
|
|
166
|
+
declare(strict_types=1);
|
|
167
|
+
|
|
168
|
+
class EmailValidationIntegrationTest extends TestCase
|
|
169
|
+
{
|
|
170
|
+
private Closure $validateEmail;
|
|
171
|
+
|
|
172
|
+
protected function setUp(): void
|
|
173
|
+
{
|
|
174
|
+
$blocked = ['spam.com'];
|
|
175
|
+
$this->validateEmail = createEmailValidator($blocked);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
#[DataProvider('integrationProvider')]
|
|
179
|
+
public function testFullValidation(string $input, array $expected): void
|
|
180
|
+
{
|
|
181
|
+
$result = ($this->validateEmail)($input);
|
|
182
|
+
$this->assertSame($expected['success'], $result['success']);
|
|
183
|
+
if (!$expected['success']) {
|
|
184
|
+
$this->assertStringContainsString($expected['error'], $result['error']);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
public static function integrationProvider(): array
|
|
189
|
+
{
|
|
190
|
+
return [
|
|
191
|
+
'valid email' => [
|
|
192
|
+
'USER@EXAMPLE.COM',
|
|
193
|
+
['success' => true]
|
|
194
|
+
],
|
|
195
|
+
'invalid format' => [
|
|
196
|
+
'not-an-email',
|
|
197
|
+
['success' => false, 'error' => 'format']
|
|
198
|
+
],
|
|
199
|
+
'blocked domain' => [
|
|
200
|
+
'user@spam.com',
|
|
201
|
+
['success' => false, 'error' => 'blocked']
|
|
202
|
+
],
|
|
203
|
+
];
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Testing Function Factories
|
|
209
|
+
|
|
210
|
+
```php
|
|
211
|
+
<?php
|
|
212
|
+
declare(strict_types=1);
|
|
213
|
+
|
|
214
|
+
class FunctionFactoryTest extends TestCase
|
|
215
|
+
{
|
|
216
|
+
public function testCreateValidatorReturnsCallable(): void
|
|
217
|
+
{
|
|
218
|
+
$validator = createValidator(minLength: 5);
|
|
219
|
+
$this->assertIsCallable($validator);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
public function testFactoryConfigurationIsCaptured(): void
|
|
223
|
+
{
|
|
224
|
+
$validator5 = createValidator(minLength: 5);
|
|
225
|
+
$validator10 = createValidator(minLength: 10);
|
|
226
|
+
|
|
227
|
+
// Same input, different results based on configuration
|
|
228
|
+
$this->assertTrue($validator5('hello')); // 5 >= 5
|
|
229
|
+
$this->assertFalse($validator10('hello')); // 5 < 10
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
public function testFactoryProducesIndependentInstances(): void
|
|
233
|
+
{
|
|
234
|
+
$v1 = createValidator(minLength: 3);
|
|
235
|
+
$v2 = createValidator(minLength: 3);
|
|
236
|
+
|
|
237
|
+
// Different instances, same behavior
|
|
238
|
+
$this->assertNotSame($v1, $v2);
|
|
239
|
+
$this->assertSame($v1('abc'), $v2('abc'));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Result Type Testing
|
|
245
|
+
|
|
246
|
+
```php
|
|
247
|
+
<?php
|
|
248
|
+
declare(strict_types=1);
|
|
249
|
+
|
|
250
|
+
class ResultTypeTest extends TestCase
|
|
251
|
+
{
|
|
252
|
+
public function testSuccessResultStructure(): void
|
|
253
|
+
{
|
|
254
|
+
$result = divide(10.0, 2.0);
|
|
255
|
+
|
|
256
|
+
$this->assertArrayHasKey('success', $result);
|
|
257
|
+
$this->assertTrue($result['success']);
|
|
258
|
+
$this->assertArrayHasKey('data', $result);
|
|
259
|
+
$this->assertSame(5.0, $result['data']);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
public function testFailureResultStructure(): void
|
|
263
|
+
{
|
|
264
|
+
$result = divide(10.0, 0.0);
|
|
265
|
+
|
|
266
|
+
$this->assertArrayHasKey('success', $result);
|
|
267
|
+
$this->assertFalse($result['success']);
|
|
268
|
+
$this->assertArrayHasKey('error', $result);
|
|
269
|
+
$this->assertIsString($result['error']);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
public function testResultChaining(): void
|
|
273
|
+
{
|
|
274
|
+
// Test that failures short-circuit
|
|
275
|
+
$result = processCalculation(10.0, 0.0, 5.0);
|
|
276
|
+
|
|
277
|
+
$this->assertFalse($result['success']);
|
|
278
|
+
$this->assertStringContainsString('zero', $result['error']);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## Mocking Dependencies
|
|
284
|
+
|
|
285
|
+
```php
|
|
286
|
+
<?php
|
|
287
|
+
declare(strict_types=1);
|
|
288
|
+
|
|
289
|
+
class ServiceWithDependenciesTest extends TestCase
|
|
290
|
+
{
|
|
291
|
+
public function testUserServiceWithMocks(): void
|
|
292
|
+
{
|
|
293
|
+
// Create mock implementations
|
|
294
|
+
$mockHasher = $this->createMock(PasswordHasherInterface::class);
|
|
295
|
+
$mockHasher->method('hash')->willReturn('hashed_password');
|
|
296
|
+
|
|
297
|
+
$mockDb = $this->createMock(DatabaseInterface::class);
|
|
298
|
+
$mockDb->method('save')->willReturn(['id' => 1, 'name' => 'Test']);
|
|
299
|
+
|
|
300
|
+
// Inject mocks - DI makes this trivial
|
|
301
|
+
$result = saveUser(
|
|
302
|
+
['name' => 'Test', 'password' => 'secret'],
|
|
303
|
+
$mockHasher,
|
|
304
|
+
$mockDb
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
$this->assertSame(1, $result['id']);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
public function testPureFunctionNeedsNoMocks(): void
|
|
311
|
+
{
|
|
312
|
+
// Pure functions are trivially testable
|
|
313
|
+
$result = calculateDiscount(100.0, 0.1);
|
|
314
|
+
$this->assertSame(90.0, $result);
|
|
315
|
+
// No setup, no mocks, no cleanup
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## Performance Testing Pure Functions
|
|
321
|
+
|
|
322
|
+
```php
|
|
323
|
+
<?php
|
|
324
|
+
declare(strict_types=1);
|
|
325
|
+
|
|
326
|
+
class PerformanceTest extends TestCase
|
|
327
|
+
{
|
|
328
|
+
public function testFactoryPerformanceGain(): void
|
|
329
|
+
{
|
|
330
|
+
$data = range(1, 10000);
|
|
331
|
+
$config = ['multiplier' => 2];
|
|
332
|
+
|
|
333
|
+
// Without factory: config accessed each iteration
|
|
334
|
+
$start = microtime(true);
|
|
335
|
+
$results1 = array_map(
|
|
336
|
+
fn($n) => $n * $config['multiplier'],
|
|
337
|
+
$data
|
|
338
|
+
);
|
|
339
|
+
$withoutFactory = microtime(true) - $start;
|
|
340
|
+
|
|
341
|
+
// With factory: config captured once
|
|
342
|
+
$processor = fn($n) => $n * 2; // Config pre-compiled
|
|
343
|
+
$start = microtime(true);
|
|
344
|
+
$results2 = array_map($processor, $data);
|
|
345
|
+
$withFactory = microtime(true) - $start;
|
|
346
|
+
|
|
347
|
+
$this->assertSame($results1, $results2);
|
|
348
|
+
// Factory should be faster (config not re-accessed)
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Test Organization
|
|
354
|
+
|
|
355
|
+
```
|
|
356
|
+
tests/
|
|
357
|
+
Unit/
|
|
358
|
+
Validators/
|
|
359
|
+
EmailValidatorTest.php
|
|
360
|
+
RangeValidatorTest.php
|
|
361
|
+
Transformers/
|
|
362
|
+
DataMapperTest.php
|
|
363
|
+
Utils/
|
|
364
|
+
ResultTypeTest.php
|
|
365
|
+
Integration/
|
|
366
|
+
ValidationPipelineTest.php
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
**Key Principles**:
|
|
370
|
+
1. One test class per function/factory
|
|
371
|
+
2. Data providers for systematic coverage
|
|
372
|
+
3. Test components independently, then composition
|
|
373
|
+
4. Pure functions need no mocks
|
|
374
|
+
5. Edge cases are cheap to test - test them all
|