@baldurpan/create-ai-workflow 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/LICENSE +21 -0
- package/README.md +165 -0
- package/dist/agents-block.js +70 -0
- package/dist/bin.js +14 -0
- package/dist/check/markdown.js +47 -0
- package/dist/check/parse.js +113 -0
- package/dist/check/rules.js +245 -0
- package/dist/cli.js +109 -0
- package/dist/commands/check.js +41 -0
- package/dist/commands/install.js +76 -0
- package/dist/commands/standards-add.js +145 -0
- package/dist/commands/update.js +214 -0
- package/dist/layout.js +115 -0
- package/dist/log.js +20 -0
- package/dist/manifest.js +53 -0
- package/dist/paths.js +41 -0
- package/package.json +37 -0
- package/templates/blocks/agents-block.md +30 -0
- package/templates/claude/agents/planner.agent.md +60 -0
- package/templates/claude/agents/reviewer.agent.md +77 -0
- package/templates/context/README.md +81 -0
- package/templates/context/plan-template.md +69 -0
- package/templates/context/plan-template.notes.md +55 -0
- package/templates/context/roles/coder.md +76 -0
- package/templates/context/workflow.md +143 -0
- package/templates/skills/feature-close/SKILL.md +102 -0
- package/templates/skills/feature-implement/SKILL.md +142 -0
- package/templates/skills/feature-plan/SKILL.md +138 -0
- package/templates/skills/feature-status/SKILL.md +86 -0
- package/templates/skills/onboard/SKILL.md +122 -0
- package/templates/skills/orchestrate/SKILL.md +92 -0
- package/templates/skills/roadmap/SKILL.md +97 -0
- package/templates/standards/.source +8 -0
- package/templates/standards/README.md +104 -0
- package/templates/standards/architecture/api-design.md +212 -0
- package/templates/standards/architecture/dependency-boundaries.md +104 -0
- package/templates/standards/architecture/feature-driven.md +102 -0
- package/templates/standards/architecture/folder-structure.md +108 -0
- package/templates/standards/architecture/monorepos.md +107 -0
- package/templates/standards/architecture/refactoring.md +97 -0
- package/templates/standards/architecture/shared-code.md +81 -0
- package/templates/standards/docs/PHP-SPEC.md +1195 -0
- package/templates/standards/docs/SPEC.md +540 -0
- package/templates/standards/examples/bad/data-fetching.tsx +52 -0
- package/templates/standards/examples/bad/derived-state.tsx +60 -0
- package/templates/standards/examples/bad/feature-component.tsx +80 -0
- package/templates/standards/examples/bad/prop-drilling.tsx +70 -0
- package/templates/standards/examples/good/data-fetching.tsx +53 -0
- package/templates/standards/examples/good/derived-state.tsx +52 -0
- package/templates/standards/examples/good/feature-component.tsx +141 -0
- package/templates/standards/examples/good/invoice.schema.ts +20 -0
- package/templates/standards/examples/good/prop-drilling.tsx +83 -0
- package/templates/standards/philosophy/ai-agent-behavior.md +93 -0
- package/templates/standards/philosophy/core-principles.md +49 -0
- package/templates/standards/philosophy/incremental-abstraction.md +56 -0
- package/templates/standards/philosophy/maintainability.md +66 -0
- package/templates/standards/philosophy/readability.md +87 -0
- package/templates/standards/php/README.md +62 -0
- package/templates/standards/php/anti-patterns.md +259 -0
- package/templates/standards/php/architecture.md +241 -0
- package/templates/standards/php/frameworks.md +197 -0
- package/templates/standards/php/rules.md +205 -0
- package/templates/standards/php/security.md +190 -0
- package/templates/standards/php/testing.md +139 -0
- package/templates/standards/php/tooling.md +245 -0
- package/templates/standards/react/accessibility.md +130 -0
- package/templates/standards/react/anti-patterns.md +187 -0
- package/templates/standards/react/component-design.md +139 -0
- package/templates/standards/react/error-boundaries.md +195 -0
- package/templates/standards/react/forms.md +147 -0
- package/templates/standards/react/hooks.md +88 -0
- package/templates/standards/react/memoization.md +87 -0
- package/templates/standards/react/state-management.md +123 -0
- package/templates/standards/react/testing.md +118 -0
- package/templates/standards/react/use-effect.md +110 -0
- package/templates/standards/security/api-security.md +136 -0
- package/templates/standards/security/auth.md +112 -0
- package/templates/standards/security/secrets.md +105 -0
- package/templates/standards/security/validation.md +89 -0
- package/templates/standards/templates/.editorconfig +16 -0
- package/templates/standards/templates/.nvmrc +1 -0
- package/templates/standards/templates/AGENTS.md +46 -0
- package/templates/standards/templates/CLAUDE.md +3 -0
- package/templates/standards/templates/_dot_gitignore +50 -0
- package/templates/standards/templates/biome.json +53 -0
- package/templates/standards/templates/eslint.config.js +61 -0
- package/templates/standards/templates/project-readme.md +76 -0
- package/templates/standards/templates/pull-request-template.md +46 -0
- package/templates/standards/templates/tsconfig.json +35 -0
- package/templates/standards/tooling/biome.md +92 -0
- package/templates/standards/tooling/ci.md +128 -0
- package/templates/standards/tooling/dates.md +194 -0
- package/templates/standards/tooling/dependencies.md +90 -0
- package/templates/standards/tooling/nx.md +113 -0
- package/templates/standards/tooling/observability.md +183 -0
- package/templates/standards/tooling/prisma.md +152 -0
- package/templates/standards/tooling/shadcn.md +115 -0
- package/templates/standards/tooling/tailwind.md +135 -0
- package/templates/standards/tooling/tanstack-query.md +214 -0
- package/templates/standards/tooling/tanstack-router.md +365 -0
- package/templates/standards/tooling/tanstack.md +31 -0
- package/templates/standards/tooling/vite.md +135 -0
- package/templates/standards/typescript/anti-patterns.md +107 -0
- package/templates/standards/typescript/error-handling.md +216 -0
- package/templates/standards/typescript/naming.md +78 -0
- package/templates/standards/typescript/rules.md +159 -0
- package/templates/standards/typescript/tsconfig/base.json +32 -0
- package/templates/standards/typescript/tsconfig/nextjs.json +12 -0
- package/templates/standards/typescript/tsconfig/react.json +9 -0
- package/templates/standards/typescript/validation.md +104 -0
- package/templates/stubs/executors.md +49 -0
- package/templates/stubs/findings.md +40 -0
- package/templates/stubs/history.md +13 -0
- package/templates/stubs/roadmap.md +29 -0
- package/templates/stubs/stack.md +35 -0
- package/templates/stubs/verify.md +48 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# PHP Testing
|
|
2
|
+
|
|
3
|
+
## Tooling
|
|
4
|
+
|
|
5
|
+
- **Pest** — preferred for new projects; expressive syntax, great DX
|
|
6
|
+
- **PHPUnit** — established standard; use for legacy or teams already on it
|
|
7
|
+
|
|
8
|
+
Both can coexist in the same project. New tests should follow whichever the codebase uses.
|
|
9
|
+
|
|
10
|
+
## Test Pyramid
|
|
11
|
+
|
|
12
|
+
Every modern PHP project should have:
|
|
13
|
+
|
|
14
|
+
- **Unit tests** — pure logic, value objects, domain services
|
|
15
|
+
- **Integration tests** — application handlers with real database, queue, cache (in containers)
|
|
16
|
+
- **End-to-end tests** — critical user flows via HTTP
|
|
17
|
+
|
|
18
|
+
Avoid excessive mocking. Integration tests with real (containerized) dependencies catch bugs that mock-heavy unit tests miss.
|
|
19
|
+
|
|
20
|
+
## Test Behavior, Not Implementation
|
|
21
|
+
|
|
22
|
+
```php
|
|
23
|
+
// BAD — asserts on method calls (couples test to implementation)
|
|
24
|
+
$user->expects()->method('setStatus')->with('active');
|
|
25
|
+
|
|
26
|
+
// GOOD — asserts on observable behavior
|
|
27
|
+
expect($user->status())->toBe(UserStatus::Active);
|
|
28
|
+
// or PHPUnit:
|
|
29
|
+
$this->assertEquals(UserStatus::Active, $user->status());
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
When the implementation changes but the behavior doesn't, the test should still pass.
|
|
33
|
+
|
|
34
|
+
## Pest Example
|
|
35
|
+
|
|
36
|
+
```php
|
|
37
|
+
<?php declare(strict_types=1);
|
|
38
|
+
|
|
39
|
+
use App\Domain\Order;
|
|
40
|
+
use App\Domain\Money;
|
|
41
|
+
|
|
42
|
+
it('calculates the total with tax', function () {
|
|
43
|
+
$order = new Order(amount: new Money(100, 'USD'));
|
|
44
|
+
|
|
45
|
+
$total = $order->totalWithTax(taxRate: 0.20);
|
|
46
|
+
|
|
47
|
+
expect($total)->toEqual(new Money(120, 'USD'));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('rejects negative amounts', function () {
|
|
51
|
+
expect(fn () => new Money(-1, 'USD'))
|
|
52
|
+
->toThrow(InvalidArgumentException::class);
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## PHPUnit Example
|
|
57
|
+
|
|
58
|
+
```php
|
|
59
|
+
<?php declare(strict_types=1);
|
|
60
|
+
|
|
61
|
+
namespace Tests\Domain;
|
|
62
|
+
|
|
63
|
+
use App\Domain\Order;
|
|
64
|
+
use App\Domain\Money;
|
|
65
|
+
use PHPUnit\Framework\TestCase;
|
|
66
|
+
|
|
67
|
+
final class OrderTest extends TestCase
|
|
68
|
+
{
|
|
69
|
+
public function test_calculates_total_with_tax(): void
|
|
70
|
+
{
|
|
71
|
+
$order = new Order(amount: new Money(100, 'USD'));
|
|
72
|
+
|
|
73
|
+
$total = $order->totalWithTax(taxRate: 0.20);
|
|
74
|
+
|
|
75
|
+
$this->assertEquals(new Money(120, 'USD'), $total);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Integration Tests
|
|
81
|
+
|
|
82
|
+
For integration tests, use real (containerized) dependencies — not mocks:
|
|
83
|
+
|
|
84
|
+
```php
|
|
85
|
+
it('creates an invoice and emits an event', function () {
|
|
86
|
+
$handler = $this->app->make(CreateInvoiceHandler::class);
|
|
87
|
+
|
|
88
|
+
$invoice = $handler->handle(new CreateInvoiceCommand(
|
|
89
|
+
customerId: $this->testCustomer->id,
|
|
90
|
+
amount: new Money(500, 'USD'),
|
|
91
|
+
));
|
|
92
|
+
|
|
93
|
+
// Asserts against the real database
|
|
94
|
+
expect(Invoice::find($invoice->id))->not->toBeNull();
|
|
95
|
+
|
|
96
|
+
// Asserts the event was dispatched
|
|
97
|
+
Event::assertDispatched(InvoiceCreated::class);
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Test Factories
|
|
102
|
+
|
|
103
|
+
Use factories (Laravel) or fixtures (Symfony) for test data — never hand-construct entities row by row:
|
|
104
|
+
|
|
105
|
+
```php
|
|
106
|
+
$user = User::factory()->create(['status' => UserStatus::Active]);
|
|
107
|
+
$invoices = Invoice::factory()->count(5)->for($user)->create();
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Coverage Expectations
|
|
111
|
+
|
|
112
|
+
- Domain layer: aim for high coverage (it's pure logic)
|
|
113
|
+
- Application handlers: integration tests covering happy and error paths
|
|
114
|
+
- Infrastructure adapters: minimal — test the adapter contract, not the third-party library
|
|
115
|
+
- Presentation: tested via HTTP-level integration tests, not controller unit tests
|
|
116
|
+
|
|
117
|
+
Coverage percentage is a weak proxy. Prefer "every critical flow has at least one integration test" over "85% coverage."
|
|
118
|
+
|
|
119
|
+
## DO NOT
|
|
120
|
+
|
|
121
|
+
- Mock things you own — refactor the design instead
|
|
122
|
+
- Use `partialMock`, `Mockery::spy`, or method-call assertions for normal logic
|
|
123
|
+
- Write tests that depend on order
|
|
124
|
+
- Share mutable state between tests
|
|
125
|
+
- Skip writing tests because "it's just a small change"
|
|
126
|
+
|
|
127
|
+
## PRIORITY
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
Integration tests with real dependencies > Mock-heavy unit tests
|
|
131
|
+
Behavior assertions > Method-call assertions
|
|
132
|
+
Critical-flow coverage > Coverage percentage
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## See Also
|
|
136
|
+
|
|
137
|
+
- [`rules.md`](rules.md) — typed, testable code by construction
|
|
138
|
+
- [`tooling.md`](tooling.md) — Pest, PHPUnit, CI integration
|
|
139
|
+
- [`../react/testing.md`](../react/testing.md) — same philosophy for the frontend
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# PHP Tooling
|
|
2
|
+
|
|
3
|
+
## Core Stack
|
|
4
|
+
|
|
5
|
+
| Tool | Purpose | Required? |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| **Composer** | Dependency management, PSR-4 autoloading | Yes |
|
|
8
|
+
| **PHPStan** | Static analysis | Yes — level max |
|
|
9
|
+
| **Pint** or **PHP-CS-Fixer** | Formatting | Yes |
|
|
10
|
+
| **Pest** or **PHPUnit** | Testing | Yes |
|
|
11
|
+
| **Rector** | Automated refactors, PHP/framework upgrades | Strongly recommended |
|
|
12
|
+
| **Docker** | Containerized dev environment | Strongly recommended |
|
|
13
|
+
| **Xdebug** | Step debugging | Optional but valuable |
|
|
14
|
+
|
|
15
|
+
## Composer
|
|
16
|
+
|
|
17
|
+
Never manually `include` files. Use Composer for all autoloading and dependencies.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
composer install # install from composer.lock
|
|
21
|
+
composer require <pkg> # add a runtime dependency
|
|
22
|
+
composer require --dev <pkg> # dev-only
|
|
23
|
+
composer update # update within composer.json constraints
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
PSR-4 autoload mapping:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"autoload": {
|
|
31
|
+
"psr-4": {
|
|
32
|
+
"App\\": "src/"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"autoload-dev": {
|
|
36
|
+
"psr-4": {
|
|
37
|
+
"Tests\\": "tests/"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
After changing autoload config, regenerate:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
composer dump-autoload --optimize
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Always commit `composer.lock`.
|
|
50
|
+
|
|
51
|
+
## PHPStan
|
|
52
|
+
|
|
53
|
+
Static analysis is **mandatory**. Modern PHP without it is incomplete.
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
composer require --dev phpstan/phpstan
|
|
57
|
+
|
|
58
|
+
# Run
|
|
59
|
+
vendor/bin/phpstan analyse
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`phpstan.neon` baseline:
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
parameters:
|
|
66
|
+
level: max
|
|
67
|
+
paths:
|
|
68
|
+
- src
|
|
69
|
+
- tests
|
|
70
|
+
excludePaths:
|
|
71
|
+
- vendor
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Target **level max** (level 10 as of PHPStan 2). If you're adopting PHPStan into an existing codebase, generate a baseline to suppress existing errors and improve from there:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
vendor/bin/phpstan analyse --generate-baseline
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Then fix baseline entries over time.
|
|
81
|
+
|
|
82
|
+
### Useful PHPStan Extensions
|
|
83
|
+
|
|
84
|
+
- `phpstan/phpstan-strict-rules` — additional strictness
|
|
85
|
+
- `phpstan/phpstan-deprecation-rules` — flag deprecated API use
|
|
86
|
+
- Framework extensions: `nunomaduro/larastan` (Laravel), `phpstan/phpstan-symfony`
|
|
87
|
+
- `phpstan/phpstan-phpunit` or `pestphp/pest-plugin-phpstan` for test files
|
|
88
|
+
|
|
89
|
+
### Psalm Alternative
|
|
90
|
+
|
|
91
|
+
Psalm is an excellent alternative to PHPStan. Pick one; don't run both.
|
|
92
|
+
|
|
93
|
+
## Pint / PHP-CS-Fixer
|
|
94
|
+
|
|
95
|
+
Pick one formatter and let it own style entirely. Never argue about formatting in PRs.
|
|
96
|
+
|
|
97
|
+
**Pint** (Laravel-flavored, simpler config):
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
composer require --dev laravel/pint
|
|
101
|
+
|
|
102
|
+
vendor/bin/pint # format
|
|
103
|
+
vendor/bin/pint --test # check without writing (CI)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**PHP-CS-Fixer** (more configurable):
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
composer require --dev friendsofphp/php-cs-fixer
|
|
110
|
+
|
|
111
|
+
vendor/bin/php-cs-fixer fix
|
|
112
|
+
vendor/bin/php-cs-fixer fix --dry-run --diff # CI
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Both default to PSR-12.
|
|
116
|
+
|
|
117
|
+
## Testing — Pest or PHPUnit
|
|
118
|
+
|
|
119
|
+
See [`testing.md`](testing.md) for the testing philosophy. Setup:
|
|
120
|
+
|
|
121
|
+
**Pest:**
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
composer require --dev pestphp/pest pestphp/pest-plugin-laravel # if Laravel
|
|
125
|
+
vendor/bin/pest --init
|
|
126
|
+
|
|
127
|
+
vendor/bin/pest
|
|
128
|
+
vendor/bin/pest --coverage
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**PHPUnit:**
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
composer require --dev phpunit/phpunit
|
|
135
|
+
|
|
136
|
+
vendor/bin/phpunit
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Rector
|
|
140
|
+
|
|
141
|
+
Rector automates refactors — PHP version upgrades, framework upgrades, code modernization. Extremely valuable.
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
composer require --dev rector/rector
|
|
145
|
+
|
|
146
|
+
# Config in rector.php (sets paths, rules, php version target)
|
|
147
|
+
vendor/bin/rector process
|
|
148
|
+
vendor/bin/rector process --dry-run # preview without writing
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Common rule sets:
|
|
152
|
+
- `LevelSetList::UP_TO_PHP_83`
|
|
153
|
+
- `SetList::CODE_QUALITY`
|
|
154
|
+
- `SetList::DEAD_CODE`
|
|
155
|
+
- `SetList::TYPE_DECLARATION`
|
|
156
|
+
|
|
157
|
+
## Docker
|
|
158
|
+
|
|
159
|
+
Dockerize local dev. Avoid "works on my machine" setups.
|
|
160
|
+
|
|
161
|
+
Typical `docker-compose.yml` services:
|
|
162
|
+
|
|
163
|
+
```yaml
|
|
164
|
+
services:
|
|
165
|
+
app:
|
|
166
|
+
build: .
|
|
167
|
+
volumes: ['.:/app']
|
|
168
|
+
nginx:
|
|
169
|
+
image: nginx:alpine
|
|
170
|
+
ports: ['8080:80']
|
|
171
|
+
db:
|
|
172
|
+
image: postgres:16
|
|
173
|
+
environment:
|
|
174
|
+
POSTGRES_PASSWORD: dev
|
|
175
|
+
ports: ['5432:5432']
|
|
176
|
+
redis:
|
|
177
|
+
image: redis:7-alpine
|
|
178
|
+
ports: ['6379:6379']
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
For Laravel: **Sail** wraps this; **Herd** (macOS) gives a local PHP-FPM + DB stack without containers.
|
|
182
|
+
|
|
183
|
+
## CI Pipeline (Required Gates)
|
|
184
|
+
|
|
185
|
+
Every PHP repo should run on PR:
|
|
186
|
+
|
|
187
|
+
```yaml
|
|
188
|
+
# .github/workflows/ci.yml
|
|
189
|
+
name: CI
|
|
190
|
+
on: [pull_request, push]
|
|
191
|
+
jobs:
|
|
192
|
+
check:
|
|
193
|
+
runs-on: ubuntu-latest
|
|
194
|
+
steps:
|
|
195
|
+
- uses: actions/checkout@v4
|
|
196
|
+
- uses: shivammathur/setup-php@v2
|
|
197
|
+
with:
|
|
198
|
+
php-version: '8.3'
|
|
199
|
+
coverage: none
|
|
200
|
+
tools: composer:v2
|
|
201
|
+
- run: composer install --prefer-dist --no-progress
|
|
202
|
+
- run: vendor/bin/pint --test
|
|
203
|
+
- run: vendor/bin/phpstan analyse --no-progress
|
|
204
|
+
- run: vendor/bin/pest --parallel
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
CI must fail on:
|
|
208
|
+
- Format violations
|
|
209
|
+
- PHPStan errors
|
|
210
|
+
- Test failures
|
|
211
|
+
- Security audit findings (`composer audit`)
|
|
212
|
+
|
|
213
|
+
See [`../tooling/ci.md`](../tooling/ci.md) for cross-language CI principles.
|
|
214
|
+
|
|
215
|
+
## Optional but Useful
|
|
216
|
+
|
|
217
|
+
- **Telescope** (Laravel) — local request/query/job inspector
|
|
218
|
+
- **Horizon** (Laravel) — queue dashboard
|
|
219
|
+
- **Symfony Profiler** — request profiling
|
|
220
|
+
- **Xdebug** — step debugging
|
|
221
|
+
- **PHP Insights** — code quality metrics
|
|
222
|
+
- **Deptrac** — enforce architectural boundaries
|
|
223
|
+
|
|
224
|
+
## DO NOT
|
|
225
|
+
|
|
226
|
+
- Skip PHPStan to "save time" — bugs cost more than analysis
|
|
227
|
+
- Run both Pint and PHP-CS-Fixer in the same project
|
|
228
|
+
- Edit `vendor/` files
|
|
229
|
+
- Commit `vendor/` to git
|
|
230
|
+
- Use globally-installed PHP/Composer in production — pin versions in Docker or Dockerfile
|
|
231
|
+
|
|
232
|
+
## PRIORITY
|
|
233
|
+
|
|
234
|
+
```
|
|
235
|
+
PHPStan level max > "It runs" as quality bar
|
|
236
|
+
Automated formatting > Manual style debates
|
|
237
|
+
Rector for upgrades > Manual sed across the codebase
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## See Also
|
|
241
|
+
|
|
242
|
+
- [`rules.md`](rules.md) — the rules that PHPStan helps enforce
|
|
243
|
+
- [`testing.md`](testing.md) — Pest / PHPUnit setup
|
|
244
|
+
- [`frameworks.md`](frameworks.md) — framework-specific tooling additions
|
|
245
|
+
- [`../tooling/ci.md`](../tooling/ci.md) — cross-language CI principles
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Accessibility
|
|
2
|
+
|
|
3
|
+
## Core Rule
|
|
4
|
+
|
|
5
|
+
Accessibility is not optional. It is part of the definition of done.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Semantic HTML first — use the right element for the job
|
|
10
|
+
- All interactive elements must be keyboard accessible
|
|
11
|
+
- All interactive elements must be usable by screen readers
|
|
12
|
+
- All form inputs must have associated labels
|
|
13
|
+
- Color is never the only means of conveying information
|
|
14
|
+
- Minimum contrast ratio: 4.5:1 for normal text, 3:1 for large text (WCAG AA)
|
|
15
|
+
|
|
16
|
+
## Semantic HTML
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
// BAD — no semantics
|
|
20
|
+
<div onClick={handleSubmit}>Submit</div>
|
|
21
|
+
<div className="heading">Invoice Details</div>
|
|
22
|
+
|
|
23
|
+
// GOOD — correct semantics
|
|
24
|
+
<button type="submit" onClick={handleSubmit}>Submit</button>
|
|
25
|
+
<h2>Invoice Details</h2>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Use the correct element:
|
|
29
|
+
- `<button>` for actions
|
|
30
|
+
- `<a>` for navigation
|
|
31
|
+
- `<h1>`–`<h6>` for headings (in order, no skipping)
|
|
32
|
+
- `<ul>` / `<ol>` / `<li>` for lists
|
|
33
|
+
- `<table>` for tabular data (with `<caption>`, `<th scope>`)
|
|
34
|
+
- `<nav>` for navigation regions
|
|
35
|
+
- `<main>`, `<aside>`, `<header>`, `<footer>` for landmarks
|
|
36
|
+
|
|
37
|
+
## Form Labeling
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
// BAD — no label association
|
|
41
|
+
<div>
|
|
42
|
+
<span>Email</span>
|
|
43
|
+
<input type="email" />
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
// GOOD — explicit label
|
|
47
|
+
<label htmlFor="email">Email</label>
|
|
48
|
+
<input id="email" type="email" />
|
|
49
|
+
|
|
50
|
+
// GOOD — wrapped label
|
|
51
|
+
<label>
|
|
52
|
+
Email
|
|
53
|
+
<input type="email" />
|
|
54
|
+
</label>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Never use `placeholder` as a substitute for a label.
|
|
58
|
+
|
|
59
|
+
## Keyboard Navigation
|
|
60
|
+
|
|
61
|
+
- All interactive elements must be reachable with Tab
|
|
62
|
+
- All interactive elements must be activatable with Enter or Space
|
|
63
|
+
- Dialogs must trap focus while open and restore focus on close
|
|
64
|
+
- Menus must support Arrow key navigation
|
|
65
|
+
- Escape must close dialogs, dropdowns, and menus
|
|
66
|
+
|
|
67
|
+
## ARIA — Use Only When Necessary
|
|
68
|
+
|
|
69
|
+
Prefer semantic HTML. Use ARIA only when a native element cannot express the required semantics.
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
// BAD — redundant ARIA
|
|
73
|
+
<button role="button" aria-label="Submit">Submit</button>
|
|
74
|
+
|
|
75
|
+
// GOOD — ARIA only when HTML cannot express it
|
|
76
|
+
<div role="status" aria-live="polite">{statusMessage}</div>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Common correct ARIA uses:
|
|
80
|
+
- `aria-label` / `aria-labelledby` — when visible label is absent
|
|
81
|
+
- `aria-describedby` — for additional descriptions (error messages, help text)
|
|
82
|
+
- `aria-expanded` — for toggleable regions
|
|
83
|
+
- `aria-live` — for dynamic content announcements
|
|
84
|
+
- `aria-invalid` — for form validation errors
|
|
85
|
+
|
|
86
|
+
## Images
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
// Decorative — hide from assistive technology
|
|
90
|
+
<img src="decoration.svg" alt="" />
|
|
91
|
+
|
|
92
|
+
// Informative — describe the content
|
|
93
|
+
<img src="invoice-preview.png" alt="Preview of invoice #1234 for $500" />
|
|
94
|
+
|
|
95
|
+
// Icon buttons — label the action, not the icon
|
|
96
|
+
<button aria-label="Download invoice">
|
|
97
|
+
<DownloadIcon aria-hidden="true" />
|
|
98
|
+
</button>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Focus Visibility
|
|
102
|
+
|
|
103
|
+
Never remove focus outlines without providing an equivalent visible replacement:
|
|
104
|
+
|
|
105
|
+
```css
|
|
106
|
+
/* BAD */
|
|
107
|
+
:focus { outline: none; }
|
|
108
|
+
|
|
109
|
+
/* GOOD */
|
|
110
|
+
:focus-visible {
|
|
111
|
+
outline: 2px solid var(--color-focus-ring);
|
|
112
|
+
outline-offset: 2px;
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## shadcn/ui and Radix
|
|
117
|
+
|
|
118
|
+
Prefer shadcn/ui components built on Radix UI primitives — they handle keyboard navigation and ARIA patterns correctly out of the box. Do not replace them with custom implementations unless there is a specific, documented reason.
|
|
119
|
+
|
|
120
|
+
## PRIORITY
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
Semantic HTML > ARIA > Custom implementation
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## See Also
|
|
127
|
+
|
|
128
|
+
- [`component-design.md`](component-design.md) — composable, accessible components
|
|
129
|
+
- [`forms.md`](forms.md) — label, error, and keyboard patterns
|
|
130
|
+
- [`../tooling/shadcn.md`](../tooling/shadcn.md) — Radix-based primitives with a11y built in
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# React Anti-Patterns
|
|
2
|
+
|
|
3
|
+
## Giant Components
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
// BAD — one component doing everything
|
|
7
|
+
function InvoicePage() {
|
|
8
|
+
const [invoices, setInvoices] = useState([]);
|
|
9
|
+
const [filters, setFilters] = useState({});
|
|
10
|
+
const [sortKey, setSortKey] = useState("date");
|
|
11
|
+
const [page, setPage] = useState(1);
|
|
12
|
+
const [selectedIds, setSelectedIds] = useState([]);
|
|
13
|
+
// ... 200 more lines of JSX, logic, and effects
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// GOOD — decomposed, composed
|
|
17
|
+
function InvoicePage() {
|
|
18
|
+
return (
|
|
19
|
+
<InvoiceLayout>
|
|
20
|
+
<InvoiceFilters />
|
|
21
|
+
<InvoiceTable />
|
|
22
|
+
<InvoicePagination />
|
|
23
|
+
</InvoiceLayout>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## useEffect for Derived State
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
// BAD
|
|
32
|
+
const [fullName, setFullName] = useState("");
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
setFullName(`${firstName} ${lastName}`);
|
|
35
|
+
}, [firstName, lastName]);
|
|
36
|
+
|
|
37
|
+
// GOOD
|
|
38
|
+
const fullName = `${firstName} ${lastName}`;
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## useEffect for Data Fetching
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
// BAD
|
|
45
|
+
const [user, setUser] = useState(null);
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
fetchUser(id).then(setUser);
|
|
48
|
+
}, [id]);
|
|
49
|
+
|
|
50
|
+
// GOOD
|
|
51
|
+
const { data: user } = useQuery({
|
|
52
|
+
queryKey: ["user", id],
|
|
53
|
+
queryFn: () => fetchUser(id),
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Prop Drilling
|
|
58
|
+
|
|
59
|
+
Prop drilling is when intermediate components accept props they don't use, only to forward them to a deeper child. The pain compounds as you add more shared values — every layer's signature grows.
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
// BAD — Layout and Sidebar don't use `user`, they just forward it
|
|
63
|
+
function Page({ user }: { user: User }) {
|
|
64
|
+
return <Layout user={user} />;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function Layout({ user }: { user: User }) {
|
|
68
|
+
return (
|
|
69
|
+
<div>
|
|
70
|
+
<Header />
|
|
71
|
+
<Sidebar user={user} />
|
|
72
|
+
<Content />
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function Sidebar({ user }: { user: User }) {
|
|
78
|
+
return (
|
|
79
|
+
<aside>
|
|
80
|
+
<Nav />
|
|
81
|
+
<UserMenu user={user} />
|
|
82
|
+
</aside>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function UserMenu({ user }: { user: User }) {
|
|
87
|
+
return <button>{user.name}</button>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// GOOD — Context lets UserMenu read `user` directly; intermediates carry nothing extra
|
|
91
|
+
function Page() {
|
|
92
|
+
return <Layout />;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function UserMenu() {
|
|
96
|
+
const { user } = useCurrentUser();
|
|
97
|
+
return <button>{user.name}</button>;
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
When deciding between context, composition, or a store, see [state-management.md](state-management.md).
|
|
102
|
+
|
|
103
|
+
## Defensive Memoization
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
// BAD — memoizing trivial values
|
|
107
|
+
const label = useMemo(() => `Hello, ${name}`, [name]);
|
|
108
|
+
const handleClick = useCallback(() => setOpen(true), []);
|
|
109
|
+
|
|
110
|
+
// GOOD — only memoize when there is a proven reason
|
|
111
|
+
const label = `Hello, ${name}`;
|
|
112
|
+
const handleClick = () => setOpen(true);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## State Synchronization Effects
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
// BAD — syncing state with state
|
|
119
|
+
const [search, setSearch] = useState("");
|
|
120
|
+
const [filteredItems, setFilteredItems] = useState(items);
|
|
121
|
+
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
setFilteredItems(items.filter((i) => i.name.includes(search)));
|
|
124
|
+
}, [search, items]);
|
|
125
|
+
|
|
126
|
+
// GOOD — compute during render
|
|
127
|
+
const filteredItems = items.filter((i) => i.name.includes(search));
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Hook Mini-Frameworks
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
// BAD — over-engineered hook that re-implements a library
|
|
134
|
+
function useDataFetcher<T>(url: string, options: FetcherOptions<T>) {
|
|
135
|
+
const [state, dispatch] = useReducer(fetcherReducer, initialState);
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
// 50 lines of custom fetching, caching, retry logic...
|
|
138
|
+
}, [url, options]);
|
|
139
|
+
return state;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// GOOD — use TanStack Query
|
|
143
|
+
const { data, isLoading, error } = useQuery({
|
|
144
|
+
queryKey: [url],
|
|
145
|
+
queryFn: () => fetch(url).then((r) => r.json()),
|
|
146
|
+
retry: 3,
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Index as Key
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
// BAD — unstable keys cause incorrect reconciliation
|
|
154
|
+
{items.map((item, index) => (
|
|
155
|
+
<Item key={index} {...item} />
|
|
156
|
+
))}
|
|
157
|
+
|
|
158
|
+
// GOOD — stable, unique key
|
|
159
|
+
{items.map((item) => (
|
|
160
|
+
<Item key={item.id} {...item} />
|
|
161
|
+
))}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Spreading Unknown Props onto DOM Elements
|
|
165
|
+
|
|
166
|
+
```tsx
|
|
167
|
+
// BAD — unknown props forwarded to DOM, React will warn
|
|
168
|
+
function Card({ className, ...rest }: Record<string, unknown>) {
|
|
169
|
+
return <div className={className} {...rest} />;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// GOOD — explicit allowed props
|
|
173
|
+
interface CardProps {
|
|
174
|
+
className?: string;
|
|
175
|
+
children: React.ReactNode;
|
|
176
|
+
}
|
|
177
|
+
function Card({ className, children }: CardProps) {
|
|
178
|
+
return <div className={className}>{children}</div>;
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## See Also
|
|
183
|
+
|
|
184
|
+
- [`use-effect.md`](use-effect.md) — when useEffect is wrong
|
|
185
|
+
- [`memoization.md`](memoization.md) — when memoization is wrong
|
|
186
|
+
- [`state-management.md`](state-management.md) — state hierarchy
|
|
187
|
+
- [`../examples/bad/`](../examples/bad/) — concrete anti-pattern examples
|