@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,197 @@
|
|
|
1
|
+
# PHP Frameworks
|
|
2
|
+
|
|
3
|
+
## Decision Matrix
|
|
4
|
+
|
|
5
|
+
| Project type | Recommended framework |
|
|
6
|
+
|---|---|
|
|
7
|
+
| Product team, API, internal tool, startup | **Laravel** |
|
|
8
|
+
| Large enterprise, long-lived platform, complex domain | **Symfony** |
|
|
9
|
+
| Lightweight API, custom architecture, minimal system | **Slim** |
|
|
10
|
+
| No framework needed | Plain PHP with chosen PSR components |
|
|
11
|
+
|
|
12
|
+
Pick one and stick with it. Don't mix frameworks in the same service.
|
|
13
|
+
|
|
14
|
+
## Laravel
|
|
15
|
+
|
|
16
|
+
Best for: product teams, rapid development, startups, internal systems, APIs.
|
|
17
|
+
|
|
18
|
+
### Strengths
|
|
19
|
+
|
|
20
|
+
- Excellent developer experience
|
|
21
|
+
- Mature ecosystem (Cashier, Horizon, Nova, Telescope, Sanctum)
|
|
22
|
+
- Strong tooling for queues, auth, testing
|
|
23
|
+
- Eloquent for fast development
|
|
24
|
+
|
|
25
|
+
### Risks
|
|
26
|
+
|
|
27
|
+
- Facade overuse leads to hidden coupling
|
|
28
|
+
- Magic-heavy patterns hurt long-term maintainability
|
|
29
|
+
- Fat models and fat controllers are common anti-patterns
|
|
30
|
+
- Easy to skip layered architecture and regret it later
|
|
31
|
+
|
|
32
|
+
### Laravel-Specific Patterns
|
|
33
|
+
|
|
34
|
+
#### DO
|
|
35
|
+
|
|
36
|
+
- **Form Requests** for validation — keep validation out of controllers
|
|
37
|
+
- **Service classes / Application handlers** for business logic — keep controllers thin
|
|
38
|
+
- **Jobs, Events, Listeners** for async work and decoupling
|
|
39
|
+
- **Policies** for authorization
|
|
40
|
+
- **API Resources** for response serialization
|
|
41
|
+
- **Scoped bindings** to avoid duplicated singletons
|
|
42
|
+
- **Eager loading** (`with()`) to prevent N+1 queries
|
|
43
|
+
- **Form requests + DTOs together** — Form Request validates, then maps to a DTO that the handler consumes
|
|
44
|
+
|
|
45
|
+
#### DO NOT
|
|
46
|
+
|
|
47
|
+
- Put business logic in routes (`Route::post('/...', fn () => /* logic */)`)
|
|
48
|
+
- Put business logic in controllers
|
|
49
|
+
- Put business logic in Eloquent models (keep them as data + relationships)
|
|
50
|
+
- Build massive `AppServiceProvider`s — split into focused providers
|
|
51
|
+
- Define magic macros for core business logic — use explicit classes
|
|
52
|
+
- Use global helpers (`auth()`, `request()`, `now()`) inside domain/application code — inject instead
|
|
53
|
+
|
|
54
|
+
### Example: Thin Controller + Handler + DTO
|
|
55
|
+
|
|
56
|
+
```php
|
|
57
|
+
// app/Http/Requests/CreateInvoiceRequest.php
|
|
58
|
+
final class CreateInvoiceRequest extends FormRequest
|
|
59
|
+
{
|
|
60
|
+
public function rules(): array
|
|
61
|
+
{
|
|
62
|
+
return [
|
|
63
|
+
'customer_id' => ['required', 'uuid'],
|
|
64
|
+
'amount' => ['required', 'integer', 'min:1'],
|
|
65
|
+
'currency' => ['required', 'in:USD,EUR,GBP'],
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public function toDto(): CreateInvoiceDto
|
|
70
|
+
{
|
|
71
|
+
return new CreateInvoiceDto(
|
|
72
|
+
customerId: $this->validated('customer_id'),
|
|
73
|
+
amount: new Money($this->validated('amount'), $this->validated('currency')),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// app/Application/Invoices/CreateInvoiceHandler.php
|
|
79
|
+
final readonly class CreateInvoiceHandler
|
|
80
|
+
{
|
|
81
|
+
public function __construct(
|
|
82
|
+
private InvoiceRepository $invoices,
|
|
83
|
+
private EventDispatcher $events,
|
|
84
|
+
) {}
|
|
85
|
+
|
|
86
|
+
public function handle(CreateInvoiceDto $dto): Invoice
|
|
87
|
+
{
|
|
88
|
+
$invoice = Invoice::create($dto);
|
|
89
|
+
$this->events->dispatch(new InvoiceCreated($invoice));
|
|
90
|
+
return $invoice;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// app/Http/Controllers/CreateInvoiceController.php
|
|
95
|
+
final class CreateInvoiceController
|
|
96
|
+
{
|
|
97
|
+
public function __construct(private CreateInvoiceHandler $handler) {}
|
|
98
|
+
|
|
99
|
+
public function __invoke(CreateInvoiceRequest $request): JsonResponse
|
|
100
|
+
{
|
|
101
|
+
$invoice = $this->handler->handle($request->toDto());
|
|
102
|
+
return new JsonResponse(InvoiceResource::from($invoice), 201);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Recommended Companions
|
|
108
|
+
|
|
109
|
+
- **Pest** — testing
|
|
110
|
+
- **Larastan** (`nunomaduro/larastan`) — PHPStan for Laravel
|
|
111
|
+
- **Pint** — formatting
|
|
112
|
+
- **Horizon** — queue dashboard
|
|
113
|
+
- **Telescope** — local debugging
|
|
114
|
+
- **Sanctum** — API tokens / SPA auth
|
|
115
|
+
|
|
116
|
+
## Symfony
|
|
117
|
+
|
|
118
|
+
Best for: large enterprise systems, long-lived platforms, highly structured teams, complex domains.
|
|
119
|
+
|
|
120
|
+
### Strengths
|
|
121
|
+
|
|
122
|
+
- Explicit, predictable architecture
|
|
123
|
+
- Powerful DI container with autowiring
|
|
124
|
+
- Stable, mature, conservative release cadence
|
|
125
|
+
- Reusable components (HttpFoundation, Console, Messenger) work outside the full framework
|
|
126
|
+
- Strong typing throughout
|
|
127
|
+
|
|
128
|
+
### Trade-offs
|
|
129
|
+
|
|
130
|
+
- Steeper learning curve
|
|
131
|
+
- More ceremony than Laravel
|
|
132
|
+
- Slower initial development velocity
|
|
133
|
+
|
|
134
|
+
### Symfony Patterns
|
|
135
|
+
|
|
136
|
+
- **Controllers** are services; inject dependencies via constructor
|
|
137
|
+
- **Form component** for HTML forms; **Validator component** for any validation
|
|
138
|
+
- **Messenger** for commands, queries, async messages
|
|
139
|
+
- **API Platform** for fast typed REST/GraphQL APIs
|
|
140
|
+
- **Doctrine** for the ORM (more explicit than Eloquent, better for complex domains)
|
|
141
|
+
|
|
142
|
+
### Recommended Companions
|
|
143
|
+
|
|
144
|
+
- **Pest** or **PHPUnit**
|
|
145
|
+
- **PHPStan** with `phpstan/phpstan-symfony`
|
|
146
|
+
- **PHP-CS-Fixer** with Symfony preset
|
|
147
|
+
- **API Platform** for typed APIs
|
|
148
|
+
|
|
149
|
+
## Slim
|
|
150
|
+
|
|
151
|
+
Best for: lightweight APIs, custom architectures, embedded services, projects that need maximum control with minimum framework opinions.
|
|
152
|
+
|
|
153
|
+
### Strengths
|
|
154
|
+
|
|
155
|
+
- Tiny, PSR-7/PSR-15 native
|
|
156
|
+
- You assemble the pieces (DI container, ORM, validation) yourself
|
|
157
|
+
- Zero magic
|
|
158
|
+
|
|
159
|
+
### Trade-offs
|
|
160
|
+
|
|
161
|
+
- You build everything beyond routing
|
|
162
|
+
- Less ecosystem support than Laravel/Symfony
|
|
163
|
+
|
|
164
|
+
### When to Pick Slim Over Plain PHP
|
|
165
|
+
|
|
166
|
+
If you need routing + middleware and want PSR conformance without committing to a full framework.
|
|
167
|
+
|
|
168
|
+
## No Framework
|
|
169
|
+
|
|
170
|
+
For very small services (a single endpoint, a CLI tool), plain PHP with carefully chosen libraries is fine:
|
|
171
|
+
|
|
172
|
+
- Routing: `nikic/fast-route`
|
|
173
|
+
- HTTP: `guzzlehttp/psr7`, `laminas/laminas-diactoros`
|
|
174
|
+
- DI: `php-di/php-di`
|
|
175
|
+
- Validation: `symfony/validator` or `respect/validation`
|
|
176
|
+
- ORM: `doctrine/dbal` (lightweight) or just PDO
|
|
177
|
+
|
|
178
|
+
## DO NOT
|
|
179
|
+
|
|
180
|
+
- Pick a framework based on tutorials you've seen recently — pick based on project shape and team experience
|
|
181
|
+
- Mix frameworks (Laravel + Symfony components fight each other's conventions)
|
|
182
|
+
- Build a custom mini-framework when Slim or Symfony covers the use case
|
|
183
|
+
- Skip a framework "because it's just a small project" and reimplement routing, DI, validation, and logging from scratch
|
|
184
|
+
|
|
185
|
+
## PRIORITY
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
Right framework for the project shape > Familiar framework
|
|
189
|
+
Thin controllers + handlers > Fat controllers
|
|
190
|
+
Symfony Messenger / Laravel Jobs > Inline synchronous processing
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## See Also
|
|
194
|
+
|
|
195
|
+
- [`architecture.md`](architecture.md) — layered architecture works under any framework
|
|
196
|
+
- [`tooling.md`](tooling.md) — framework-specific tooling (Larastan, Symfony PHPStan)
|
|
197
|
+
- [`anti-patterns.md`](anti-patterns.md) — fat controllers, facade abuse
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# PHP Rules
|
|
2
|
+
|
|
3
|
+
## DO
|
|
4
|
+
|
|
5
|
+
- `declare(strict_types=1);` at the top of every PHP file
|
|
6
|
+
- Type every parameter, return value, and property
|
|
7
|
+
- Use modern PHP 8.3+ features: constructor property promotion, readonly properties/classes, enums, match expressions, attributes, union/intersection types, first-class callable syntax
|
|
8
|
+
- Use value objects for domain concepts (Money, EmailAddress, UserId) — not raw strings/ints
|
|
9
|
+
- Use DTOs for API/queue payloads — readonly classes, no setters
|
|
10
|
+
- Follow PSR-12 formatting (auto-enforced by Pint or PHP-CS-Fixer)
|
|
11
|
+
- Follow PSR-4 autoloading via Composer
|
|
12
|
+
- Use PSR-3 (`LoggerInterface`) for logging — not custom logger classes
|
|
13
|
+
- Name things by responsibility (`CalculateInvoiceTotals`) — not by role (`InvoiceManager`)
|
|
14
|
+
|
|
15
|
+
## DO NOT
|
|
16
|
+
|
|
17
|
+
- Use untyped parameters or return values
|
|
18
|
+
- Pass `array` as a data contract — use DTOs or value objects
|
|
19
|
+
- Use generic class names: `Manager`, `Helper`, `Util`, `Common`, `Base`, `Handler` (unless it's actually a PSR-15 handler)
|
|
20
|
+
- Use deep inheritance chains — prefer composition
|
|
21
|
+
- Use traits for hidden cross-cutting behavior — prefer composition
|
|
22
|
+
- Use global state, service locators, static singletons, or facade abuse
|
|
23
|
+
- Reach for magic methods (`__call`, `__get`) when explicit code works
|
|
24
|
+
- Mix HTML and PHP in the same file (use Blade, Twig, or a frontend)
|
|
25
|
+
|
|
26
|
+
## Strict Types
|
|
27
|
+
|
|
28
|
+
```php
|
|
29
|
+
<?php declare(strict_types=1);
|
|
30
|
+
|
|
31
|
+
namespace App\Billing;
|
|
32
|
+
|
|
33
|
+
final class OrderService
|
|
34
|
+
{
|
|
35
|
+
public function calculateTotal(Order $order): Money
|
|
36
|
+
{
|
|
37
|
+
// ...
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Without `strict_types`, PHP silently coerces types — `int(5)` becomes `string("5")`, `string("abc")` becomes `int(0)`. Strict types make these errors loud at the call site.
|
|
43
|
+
|
|
44
|
+
## Type Everything
|
|
45
|
+
|
|
46
|
+
```php
|
|
47
|
+
// BAD — implicit any
|
|
48
|
+
public function process($order)
|
|
49
|
+
{
|
|
50
|
+
return $order->total;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// GOOD — explicit input and output types
|
|
54
|
+
public function process(Order $order): Money
|
|
55
|
+
{
|
|
56
|
+
return $order->total;
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
For collections, use PHPStan/Psalm generics annotations since PHP doesn't have native generics:
|
|
61
|
+
|
|
62
|
+
```php
|
|
63
|
+
/**
|
|
64
|
+
* @param list<Order> $orders
|
|
65
|
+
* @return list<Money>
|
|
66
|
+
*/
|
|
67
|
+
public function extractTotals(array $orders): array
|
|
68
|
+
{
|
|
69
|
+
return array_map(fn (Order $o) => $o->total, $orders);
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Modern Features in Action
|
|
74
|
+
|
|
75
|
+
### Constructor Property Promotion
|
|
76
|
+
|
|
77
|
+
```php
|
|
78
|
+
// GOOD — concise, no boilerplate
|
|
79
|
+
final class OrderService
|
|
80
|
+
{
|
|
81
|
+
public function __construct(
|
|
82
|
+
private readonly PaymentGateway $payments,
|
|
83
|
+
private readonly OrderRepository $orders,
|
|
84
|
+
) {}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// BAD — manual property declaration and assignment
|
|
88
|
+
final class OrderService
|
|
89
|
+
{
|
|
90
|
+
private PaymentGateway $payments;
|
|
91
|
+
private OrderRepository $orders;
|
|
92
|
+
|
|
93
|
+
public function __construct(PaymentGateway $payments, OrderRepository $orders)
|
|
94
|
+
{
|
|
95
|
+
$this->payments = $payments;
|
|
96
|
+
$this->orders = $orders;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Readonly Classes (PHP 8.2+)
|
|
102
|
+
|
|
103
|
+
```php
|
|
104
|
+
final readonly class Money
|
|
105
|
+
{
|
|
106
|
+
public function __construct(
|
|
107
|
+
public int $amount,
|
|
108
|
+
public string $currency,
|
|
109
|
+
) {}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
All properties are immutable after construction. Add a method that returns a new instance for "mutations":
|
|
114
|
+
|
|
115
|
+
```php
|
|
116
|
+
public function add(Money $other): Money
|
|
117
|
+
{
|
|
118
|
+
return new Money($this->amount + $other->amount, $this->currency);
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Enums
|
|
123
|
+
|
|
124
|
+
```php
|
|
125
|
+
enum InvoiceStatus: string
|
|
126
|
+
{
|
|
127
|
+
case Draft = 'draft';
|
|
128
|
+
case Open = 'open';
|
|
129
|
+
case Paid = 'paid';
|
|
130
|
+
case Void = 'void';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Pattern match
|
|
134
|
+
$label = match ($invoice->status) {
|
|
135
|
+
InvoiceStatus::Draft => 'Draft',
|
|
136
|
+
InvoiceStatus::Open => 'Awaiting payment',
|
|
137
|
+
InvoiceStatus::Paid => 'Paid',
|
|
138
|
+
InvoiceStatus::Void => 'Voided',
|
|
139
|
+
};
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Value Objects Over Primitives
|
|
143
|
+
|
|
144
|
+
```php
|
|
145
|
+
// BAD — primitive obsession; what's $email's shape? validated?
|
|
146
|
+
public function createUser(string $email, string $name): User
|
|
147
|
+
|
|
148
|
+
// GOOD — types enforce the contract
|
|
149
|
+
public function createUser(EmailAddress $email, FullName $name): User
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Common value objects to introduce early:
|
|
153
|
+
- `EmailAddress`, `PhoneNumber`, `Url`
|
|
154
|
+
- `Money`, `Currency`, `Percentage`
|
|
155
|
+
- `UserId`, `OrderId`, `InvoiceId` (typed IDs prevent passing the wrong one)
|
|
156
|
+
- `DateRange`, `TimeOfDay`
|
|
157
|
+
|
|
158
|
+
## DTOs
|
|
159
|
+
|
|
160
|
+
For API requests, queue payloads, and service-to-service messages:
|
|
161
|
+
|
|
162
|
+
```php
|
|
163
|
+
final readonly class CreateUserDTO
|
|
164
|
+
{
|
|
165
|
+
public function __construct(
|
|
166
|
+
public string $name,
|
|
167
|
+
public EmailAddress $email,
|
|
168
|
+
) {}
|
|
169
|
+
|
|
170
|
+
public static function fromRequest(Request $request): self
|
|
171
|
+
{
|
|
172
|
+
return new self(
|
|
173
|
+
name: $request->validated('name'),
|
|
174
|
+
email: new EmailAddress($request->validated('email')),
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Naming
|
|
181
|
+
|
|
182
|
+
| Pattern | Convention | Example |
|
|
183
|
+
|---|---|---|
|
|
184
|
+
| Classes | `PascalCase` | `OrderService`, `EmailAddress` |
|
|
185
|
+
| Methods | `camelCase` | `calculateTotal`, `findActiveSubscribers` |
|
|
186
|
+
| Properties | `camelCase` | `$totalAmount` |
|
|
187
|
+
| Constants | `SCREAMING_SNAKE_CASE` | `MAX_RETRIES` |
|
|
188
|
+
| Interfaces | `PascalCase`, no `I` prefix | `PaymentGateway` not `IPaymentGateway` |
|
|
189
|
+
| Namespaces | match folder structure (PSR-4) | `App\Billing\OrderService` in `src/Billing/OrderService.php` |
|
|
190
|
+
|
|
191
|
+
## PRIORITY
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
Strict types > Loose types
|
|
195
|
+
Value objects > Primitive obsession
|
|
196
|
+
DTOs > Arrays as contracts
|
|
197
|
+
Composition > Inheritance
|
|
198
|
+
Explicit > Magic
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## See Also
|
|
202
|
+
|
|
203
|
+
- [`anti-patterns.md`](anti-patterns.md) — what to avoid
|
|
204
|
+
- [`architecture.md`](architecture.md) — where typed code lives
|
|
205
|
+
- [`tooling.md`](tooling.md) — PHPStan, Pint, Rector
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# PHP Security
|
|
2
|
+
|
|
3
|
+
PHP-specific security non-negotiables. For cross-language concerns (secrets, auth, API security), see also [`../security/`](../security/).
|
|
4
|
+
|
|
5
|
+
## Passwords
|
|
6
|
+
|
|
7
|
+
```php
|
|
8
|
+
// Hashing — use password_hash with Argon2id (preferred) or bcrypt
|
|
9
|
+
$hash = password_hash($plaintext, PASSWORD_ARGON2ID);
|
|
10
|
+
|
|
11
|
+
// Verification — timing-safe
|
|
12
|
+
if (password_verify($plaintext, $user->password_hash)) {
|
|
13
|
+
// ...
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Rehash if algorithm or cost has been upgraded
|
|
17
|
+
if (password_needs_rehash($user->password_hash, PASSWORD_ARGON2ID)) {
|
|
18
|
+
$user->password_hash = password_hash($plaintext, PASSWORD_ARGON2ID);
|
|
19
|
+
$user->save();
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Never store plaintext passwords. Never use MD5, SHA-1, or unsalted hashes.
|
|
24
|
+
|
|
25
|
+
## SQL Injection Prevention
|
|
26
|
+
|
|
27
|
+
Always use prepared statements. Never concatenate user input into SQL.
|
|
28
|
+
|
|
29
|
+
```php
|
|
30
|
+
// SAFE — parameterized via PDO
|
|
31
|
+
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = :email");
|
|
32
|
+
$stmt->execute(['email' => $email]);
|
|
33
|
+
|
|
34
|
+
// SAFE — Eloquent / Doctrine parameterize automatically
|
|
35
|
+
$user = User::where('email', $email)->first();
|
|
36
|
+
|
|
37
|
+
// DANGEROUS — string concatenation
|
|
38
|
+
$users = $pdo->query("SELECT * FROM users WHERE email = '$email'");
|
|
39
|
+
|
|
40
|
+
// SAFE — raw queries WITH bindings if you must
|
|
41
|
+
$users = DB::select("SELECT * FROM users WHERE email = ?", [$email]);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## XSS Prevention
|
|
45
|
+
|
|
46
|
+
Escape all output rendered to HTML. Templating engines do this by default:
|
|
47
|
+
|
|
48
|
+
```php
|
|
49
|
+
// Blade — auto-escapes
|
|
50
|
+
{{ $user->name }}
|
|
51
|
+
|
|
52
|
+
// Twig — auto-escapes
|
|
53
|
+
{{ user.name }}
|
|
54
|
+
|
|
55
|
+
// Manual — escape with htmlspecialchars
|
|
56
|
+
<?= htmlspecialchars($user->name, ENT_QUOTES, 'UTF-8') ?>
|
|
57
|
+
|
|
58
|
+
// DANGEROUS — raw output
|
|
59
|
+
{!! $user->name !!} // Blade unescaped
|
|
60
|
+
{{ user.name|raw }} // Twig unescaped
|
|
61
|
+
<?= $user->name ?> // manual unescaped
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Use `{!!` or `|raw` only for trusted content you control (e.g., rendered markdown that you sanitized yourself).
|
|
65
|
+
|
|
66
|
+
## CSRF Protection
|
|
67
|
+
|
|
68
|
+
Web forms must include and validate CSRF tokens.
|
|
69
|
+
|
|
70
|
+
- **Laravel** — the `web` middleware group includes `VerifyCsrfToken`; templates use `@csrf`
|
|
71
|
+
- **Symfony** — the form component includes CSRF protection by default
|
|
72
|
+
- **Slim / custom** — use a CSRF middleware (`slim/csrf` or similar)
|
|
73
|
+
|
|
74
|
+
APIs that use session cookies need CSRF protection too. APIs using bearer tokens generally don't (the cross-origin protection comes from CORS + the token header).
|
|
75
|
+
|
|
76
|
+
## File Upload Validation
|
|
77
|
+
|
|
78
|
+
```php
|
|
79
|
+
final class UploadInvoicePdfController
|
|
80
|
+
{
|
|
81
|
+
public function __invoke(Request $request): JsonResponse
|
|
82
|
+
{
|
|
83
|
+
$validated = $request->validate([
|
|
84
|
+
'file' => [
|
|
85
|
+
'required',
|
|
86
|
+
'file',
|
|
87
|
+
'mimes:pdf',
|
|
88
|
+
'max:5120', // 5MB
|
|
89
|
+
],
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
$path = $validated['file']->store('invoices');
|
|
93
|
+
// ...
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Validate:
|
|
99
|
+
- File size (reject huge uploads early)
|
|
100
|
+
- MIME type (verify the actual content, not just the extension or `Content-Type` header)
|
|
101
|
+
- File extension (whitelist, not blacklist)
|
|
102
|
+
- Image dimensions (for images, to prevent decompression attacks)
|
|
103
|
+
|
|
104
|
+
Store uploads outside the web root or behind an authenticated handler.
|
|
105
|
+
|
|
106
|
+
## Path Traversal Prevention
|
|
107
|
+
|
|
108
|
+
```php
|
|
109
|
+
// BAD — user input flows directly into a file path
|
|
110
|
+
$filename = $_GET['file'];
|
|
111
|
+
return readfile("/uploads/$filename");
|
|
112
|
+
// Attacker: ?file=../../../etc/passwd
|
|
113
|
+
|
|
114
|
+
// GOOD — validate against an allow-list or sanitize
|
|
115
|
+
$allowed = ['report.pdf', 'invoice.pdf'];
|
|
116
|
+
if (!in_array($filename, $allowed, true)) {
|
|
117
|
+
abort(404);
|
|
118
|
+
}
|
|
119
|
+
return readfile("/uploads/$filename");
|
|
120
|
+
|
|
121
|
+
// GOOD — use realpath and verify it stays inside the allowed directory
|
|
122
|
+
$base = realpath('/uploads');
|
|
123
|
+
$path = realpath("/uploads/$filename");
|
|
124
|
+
if ($path === false || !str_starts_with($path, $base)) {
|
|
125
|
+
abort(404);
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Rate Limiting
|
|
130
|
+
|
|
131
|
+
Apply rate limits to all public APIs and especially to auth endpoints.
|
|
132
|
+
|
|
133
|
+
- **Laravel** — `throttle` middleware: `Route::middleware('throttle:60,1')->...`
|
|
134
|
+
- **Symfony** — `RateLimiter` component
|
|
135
|
+
|
|
136
|
+
```php
|
|
137
|
+
Route::post('/login', LoginController::class)
|
|
138
|
+
->middleware('throttle:5,15'); // 5 attempts per 15 minutes
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Secrets
|
|
142
|
+
|
|
143
|
+
Never commit secrets. Always load from environment:
|
|
144
|
+
|
|
145
|
+
```php
|
|
146
|
+
// Read at config layer once, not throughout the app
|
|
147
|
+
return [
|
|
148
|
+
'stripe' => [
|
|
149
|
+
'secret' => env('STRIPE_SECRET_KEY'),
|
|
150
|
+
],
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
// Access via config()
|
|
154
|
+
$stripeKey = config('services.stripe.secret');
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
For environment variable validation at startup, see [`../security/secrets.md`](../security/secrets.md).
|
|
158
|
+
|
|
159
|
+
## Session Security
|
|
160
|
+
|
|
161
|
+
- Use `HttpOnly`, `Secure`, `SameSite=Lax` (or `Strict`) cookies
|
|
162
|
+
- Regenerate session ID on login (`session_regenerate_id(true)` in raw PHP; framework handles it usually)
|
|
163
|
+
- Set a session timeout
|
|
164
|
+
- Invalidate the session on logout
|
|
165
|
+
|
|
166
|
+
## DO NOT
|
|
167
|
+
|
|
168
|
+
- Build SQL strings manually
|
|
169
|
+
- Trust `$_GET` / `$_POST` / `$_REQUEST` / `$_FILES` without validation
|
|
170
|
+
- Use `eval()` or `unserialize()` on user input
|
|
171
|
+
- Store sensitive data in `$_SESSION` without encryption if the session driver writes to disk
|
|
172
|
+
- Log passwords, tokens, or credit card data
|
|
173
|
+
- Use `Content-Type` header alone to determine file type
|
|
174
|
+
- Roll your own crypto or auth — use a battle-tested library
|
|
175
|
+
|
|
176
|
+
## PRIORITY
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
Parameterized queries > String SQL
|
|
180
|
+
Argon2id > bcrypt > anything else
|
|
181
|
+
Validated, allow-listed inputs > Defensive sanitization
|
|
182
|
+
HttpOnly cookies > localStorage for auth
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## See Also
|
|
186
|
+
|
|
187
|
+
- [`../security/secrets.md`](../security/secrets.md) — env vars, rotation, scanning
|
|
188
|
+
- [`../security/auth.md`](../security/auth.md) — auth patterns (cross-language)
|
|
189
|
+
- [`../security/api-security.md`](../security/api-security.md) — CORS, error responses, rate limiting
|
|
190
|
+
- [`rules.md`](rules.md) — strict types prevent many injection paths at compile time
|