symfonia-ai-tools 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +489 -0
- package/bin/cli.mjs +35 -0
- package/lib/installer.mjs +495 -0
- package/lib/questions.mjs +332 -0
- package/lib/ui.mjs +76 -0
- package/lib/utils.mjs +231 -0
- package/package.json +26 -0
- package/templates/base/CLAUDE.md +34 -0
- package/templates/base/_ai/_guidelines_header.md +70 -0
- package/templates/base/_ai/context/README.md +20 -0
- package/templates/base/_ai/prompts/codereview.prompt.md +324 -0
- package/templates/base/_ai/prompts/duplicate-code-analysis.prompt.md +128 -0
- package/templates/base/_ai/prompts/figma-analysis.prompt.md +155 -0
- package/templates/base/_ai/prompts/security-review.prompt.md +46 -0
- package/templates/base/_ai/skills/README.md +80 -0
- package/templates/base/_ai/skills/TEMPLATE.md +106 -0
- package/templates/base/_ai/skills/babysit-prs/SKILL.md +105 -0
- package/templates/base/_ai/skills/debug/SKILL.md +93 -0
- package/templates/base/_ai/skills/fill-worklogs/SKILL.md +158 -0
- package/templates/base/_ai/skills/hotfix/SKILL.md +52 -0
- package/templates/base/_ai/skills/jira-task/SKILL.md +170 -0
- package/templates/base/_ai/skills/my-prs/SKILL.md +78 -0
- package/templates/base/_ai/skills/pr-dashboard/SKILL.md +43 -0
- package/templates/base/_ai/skills/pr-prepare/SKILL.md +106 -0
- package/templates/base/_ai/skills/refactor/SKILL.md +87 -0
- package/templates/base/_ai/skills/write-tests/SKILL.md +109 -0
- package/templates/base/_claude/settings.local.json +37 -0
- package/templates/base/_cursor/rules/global.mdc +7 -0
- package/templates/base/_editorconfig +18 -0
- package/templates/base/_gemini/settings.json +3 -0
- package/templates/base/_github/copilot-instructions.md +1 -0
- package/templates/base/_github/pull_request_template.md +23 -0
- package/templates/base/_gitignore +22 -0
- package/templates/base/_junie/guidelines.md +1 -0
- package/templates/base/commit-instructions.md +92 -0
- package/templates/packs/docker/_ai/instructions/docker.instructions.md +193 -0
- package/templates/packs/docker/_guidelines.md +10 -0
- package/templates/packs/docker/pack.json +8 -0
- package/templates/packs/laravel/_ai/instructions/api-resource.instructions.md +251 -0
- package/templates/packs/laravel/_ai/instructions/module.instructions.md +133 -0
- package/templates/packs/laravel/_ai/instructions/service-repository.instructions.md +215 -0
- package/templates/packs/laravel/_ai/instructions/testing.instructions.md +278 -0
- package/templates/packs/laravel/_ai/skills/migration/SKILL.md +172 -0
- package/templates/packs/laravel/_ai/skills/new-endpoint/SKILL.md +165 -0
- package/templates/packs/laravel/_ai/skills/new-module/SKILL.md +208 -0
- package/templates/packs/laravel/_ai/skills/queued-job/SKILL.md +248 -0
- package/templates/packs/laravel/_ai/skills/testing-feature/SKILL.md +196 -0
- package/templates/packs/laravel/_ai/skills/testing-manual/SKILL.md +186 -0
- package/templates/packs/laravel/_ai/skills/testing-unit/SKILL.md +200 -0
- package/templates/packs/laravel/_guidelines.md +25 -0
- package/templates/packs/laravel/pack.json +6 -0
- package/templates/packs/playwright/_ai/instructions/playwright.instructions.md +219 -0
- package/templates/packs/playwright/_ai/skills/playwright/README.md +194 -0
- package/templates/packs/playwright/_ai/skills/playwright/SKILL.md +1245 -0
- package/templates/packs/playwright/_ai/skills/playwright-codereview/SKILL.md +642 -0
- package/templates/packs/playwright/_ai/skills/playwright-record/README.md +87 -0
- package/templates/packs/playwright/_ai/skills/playwright-record/SKILL.md +564 -0
- package/templates/packs/playwright/_guidelines.md +12 -0
- package/templates/packs/playwright/pack.json +9 -0
- package/templates/packs/storybook/_ai/instructions/storybook.instructions.md +181 -0
- package/templates/packs/storybook/pack.json +6 -0
- package/templates/packs/vitest/_ai/instructions/vitest.instructions.md +688 -0
- package/templates/packs/vitest/pack.json +6 -0
- package/templates/packs/vue3/_ai/instructions/api.instructions.md +163 -0
- package/templates/packs/vue3/_ai/instructions/coding-conventions.instructions.md +160 -0
- package/templates/packs/vue3/_ai/instructions/composables.instructions.md +218 -0
- package/templates/packs/vue3/_ai/instructions/forms.instructions.md +227 -0
- package/templates/packs/vue3/_ai/instructions/store.instructions.md +504 -0
- package/templates/packs/vue3/_ai/instructions/vue.instructions.md +339 -0
- package/templates/packs/vue3/_ai/skills/api-integration/SKILL.md +195 -0
- package/templates/packs/vue3/_ai/skills/new-component/SKILL.md +133 -0
- package/templates/packs/vue3/_ai/skills/new-module/SKILL.md +177 -0
- package/templates/packs/vue3/_guidelines.md +45 -0
- package/templates/packs/vue3/pack.json +11 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# AI tool configs with tokens/secrets
|
|
2
|
+
.claude/settings.local.json
|
|
3
|
+
.cursor/mcp.json
|
|
4
|
+
.mcp.json
|
|
5
|
+
|
|
6
|
+
# GSD working files
|
|
7
|
+
.planning/
|
|
8
|
+
|
|
9
|
+
# Environment
|
|
10
|
+
.env
|
|
11
|
+
.env.local
|
|
12
|
+
.env.*.local
|
|
13
|
+
|
|
14
|
+
# OS
|
|
15
|
+
.DS_Store
|
|
16
|
+
Thumbs.db
|
|
17
|
+
|
|
18
|
+
# IDE
|
|
19
|
+
.idea/
|
|
20
|
+
.vscode/
|
|
21
|
+
*.swp
|
|
22
|
+
*.swo
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Read and follow .ai/guidelines.md as mandatory context.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Commit Instructions
|
|
2
|
+
|
|
3
|
+
## Issue Number from Branch
|
|
4
|
+
|
|
5
|
+
Extract the issue number from the current branch name and use it as prefix:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
feature/{{JIRA_PREFIX}}-1234-feature-name → {{JIRA_PREFIX}}-1234: description
|
|
9
|
+
bugfix/{{JIRA_PREFIX}}-567-fix-name → {{JIRA_PREFIX}}-567: description
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Format
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
{{JIRA_PREFIX}}-XXXX: <type>(<scope>): <short description>
|
|
16
|
+
|
|
17
|
+
[optional longer description — explain WHY, not WHAT]
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Types
|
|
21
|
+
|
|
22
|
+
- `feat` - new feature
|
|
23
|
+
- `fix` - bug fix
|
|
24
|
+
- `refactor` - code restructuring (no behavior change)
|
|
25
|
+
- `test` - adding/changing tests
|
|
26
|
+
- `docs` - documentation
|
|
27
|
+
- `style` - formatting (no logic change)
|
|
28
|
+
- `chore` - build/CI/tooling changes
|
|
29
|
+
|
|
30
|
+
## Scope
|
|
31
|
+
|
|
32
|
+
Module or component name: `auth`, `api`, `ui`, `store`, `config`, `db`
|
|
33
|
+
|
|
34
|
+
## Rules
|
|
35
|
+
|
|
36
|
+
1. **Explain WHY, not WHAT** — don't repeat what's visible in the diff
|
|
37
|
+
2. **Max 72 chars** in the first line
|
|
38
|
+
3. **One commit = one logical change** — split unrelated changes into separate commits
|
|
39
|
+
4. **Never add `Co-Authored-By`, `Generated by`, or any AI attribution**
|
|
40
|
+
5. **Do not commit without developer approval**
|
|
41
|
+
6. **Language**: English
|
|
42
|
+
|
|
43
|
+
## Examples
|
|
44
|
+
|
|
45
|
+
### Good
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
{{JIRA_PREFIX}}-456: feat(auth): add JWT refresh token rotation
|
|
49
|
+
|
|
50
|
+
Implement automatic token rotation on each refresh to prevent
|
|
51
|
+
token replay attacks. Tokens are invalidated after single use.
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
{{JIRA_PREFIX}}-789: fix(api): prevent race condition in concurrent updates
|
|
56
|
+
|
|
57
|
+
Multiple simultaneous PATCH requests could overwrite each other.
|
|
58
|
+
Added optimistic locking with version field comparison.
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Bad
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
# Too vague — no WHY
|
|
65
|
+
fix(auth): fixed bug
|
|
66
|
+
|
|
67
|
+
# Repeats the diff — no WHY
|
|
68
|
+
feat(users): added email field to form and regex validation
|
|
69
|
+
|
|
70
|
+
# Two unrelated changes in one commit
|
|
71
|
+
feat(users): add validation and change form layout
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Branch Naming
|
|
75
|
+
|
|
76
|
+
| Type | Pattern |
|
|
77
|
+
|------|---------|
|
|
78
|
+
| Feature | `feature/{{JIRA_PREFIX}}-XXXX-short-name` |
|
|
79
|
+
| Bugfix | `bugfix/{{JIRA_PREFIX}}-XXXX-short-name` |
|
|
80
|
+
| Hotfix | `hotfix/{{JIRA_PREFIX}}-XXXX-short-name` |
|
|
81
|
+
| Release | `release/vX.Y.Z` |
|
|
82
|
+
|
|
83
|
+
## Breaking Changes
|
|
84
|
+
|
|
85
|
+
Add `BREAKING CHANGE:` in footer when breaking backward compatibility:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
{{JIRA_PREFIX}}-321: refactor(api)!: change auth endpoint response format
|
|
89
|
+
|
|
90
|
+
BREAKING CHANGE: /api/auth/login now returns { token, expiresAt }
|
|
91
|
+
instead of { access_token, expires_in }. All clients must update.
|
|
92
|
+
```
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "**/Dockerfile,**/docker-compose*,**/docker/**"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Docker & Deployment - Instrukcje
|
|
6
|
+
|
|
7
|
+
## Zasada nadrzedna
|
|
8
|
+
|
|
9
|
+
**Wszystkie komendy PHP/Artisan/Composer uruchamiaj wewnatrz kontenera Docker.**
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# DOBRZE
|
|
13
|
+
docker exec {{DOCKER_CONTAINER}} php artisan migrate
|
|
14
|
+
docker exec {{DOCKER_CONTAINER}} composer install
|
|
15
|
+
docker exec {{DOCKER_CONTAINER}} php artisan test
|
|
16
|
+
|
|
17
|
+
# ZLE - nigdy na hoscie
|
|
18
|
+
php artisan migrate
|
|
19
|
+
composer install
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Podstawowe komendy
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Start srodowiska
|
|
26
|
+
docker compose up -d
|
|
27
|
+
|
|
28
|
+
# Stop
|
|
29
|
+
docker compose down
|
|
30
|
+
|
|
31
|
+
# Rebuild
|
|
32
|
+
docker compose up -d --build
|
|
33
|
+
|
|
34
|
+
# Logi
|
|
35
|
+
docker compose logs -f app
|
|
36
|
+
|
|
37
|
+
# Shell wewnatrz kontenera
|
|
38
|
+
docker exec -it {{DOCKER_CONTAINER}} bash
|
|
39
|
+
|
|
40
|
+
# Lista kontenerow
|
|
41
|
+
docker compose ps
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Artisan w Docker
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Migracje
|
|
48
|
+
docker exec {{DOCKER_CONTAINER}} php artisan migrate
|
|
49
|
+
docker exec {{DOCKER_CONTAINER}} php artisan migrate:rollback
|
|
50
|
+
docker exec {{DOCKER_CONTAINER}} php artisan migrate:fresh --seed
|
|
51
|
+
|
|
52
|
+
# Cache
|
|
53
|
+
docker exec {{DOCKER_CONTAINER}} php artisan optimize:clear
|
|
54
|
+
docker exec {{DOCKER_CONTAINER}} php artisan config:cache
|
|
55
|
+
docker exec {{DOCKER_CONTAINER}} php artisan route:cache
|
|
56
|
+
|
|
57
|
+
# Queue
|
|
58
|
+
docker exec {{DOCKER_CONTAINER}} php artisan queue:work --tries=3
|
|
59
|
+
docker exec {{DOCKER_CONTAINER}} php artisan horizon
|
|
60
|
+
|
|
61
|
+
# Generowanie
|
|
62
|
+
docker exec {{DOCKER_CONTAINER}} php artisan make:model Feature -mfcr
|
|
63
|
+
docker exec {{DOCKER_CONTAINER}} php artisan make:test FeatureTest --unit
|
|
64
|
+
docker exec {{DOCKER_CONTAINER}} php artisan make:request StoreFeatureRequest
|
|
65
|
+
|
|
66
|
+
# Testy
|
|
67
|
+
docker exec {{DOCKER_CONTAINER}} php artisan test
|
|
68
|
+
docker exec {{DOCKER_CONTAINER}} php artisan test --filter=FeatureTest
|
|
69
|
+
docker exec {{DOCKER_CONTAINER}} php artisan test --coverage
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## docker-compose.yml - typowa struktura
|
|
73
|
+
|
|
74
|
+
```yaml
|
|
75
|
+
services:
|
|
76
|
+
app:
|
|
77
|
+
build:
|
|
78
|
+
context: .
|
|
79
|
+
dockerfile: docker/app/Dockerfile
|
|
80
|
+
volumes:
|
|
81
|
+
- ./:/var/www/html
|
|
82
|
+
- ./docker/php/local.ini:/usr/local/etc/php/conf.d/local.ini
|
|
83
|
+
ports:
|
|
84
|
+
- "${APP_PORT:-80}:80"
|
|
85
|
+
depends_on:
|
|
86
|
+
- postgres
|
|
87
|
+
- redis
|
|
88
|
+
environment:
|
|
89
|
+
PHP_IDE_CONFIG: "serverName=localhost"
|
|
90
|
+
|
|
91
|
+
horizon:
|
|
92
|
+
build:
|
|
93
|
+
context: .
|
|
94
|
+
dockerfile: docker/app/Dockerfile
|
|
95
|
+
command: php artisan horizon
|
|
96
|
+
depends_on:
|
|
97
|
+
- postgres
|
|
98
|
+
- redis
|
|
99
|
+
restart: unless-stopped
|
|
100
|
+
|
|
101
|
+
postgres:
|
|
102
|
+
image: postgres:16-alpine
|
|
103
|
+
environment:
|
|
104
|
+
POSTGRES_DB: "${DB_DATABASE}"
|
|
105
|
+
POSTGRES_USER: "${DB_USERNAME}"
|
|
106
|
+
POSTGRES_PASSWORD: "${DB_PASSWORD}"
|
|
107
|
+
ports:
|
|
108
|
+
- "${DB_PORT:-5432}:5432"
|
|
109
|
+
volumes:
|
|
110
|
+
- postgres-data:/var/lib/postgresql/data
|
|
111
|
+
|
|
112
|
+
redis:
|
|
113
|
+
image: redis:alpine
|
|
114
|
+
ports:
|
|
115
|
+
- "${REDIS_PORT:-6379}:6379"
|
|
116
|
+
|
|
117
|
+
mailpit:
|
|
118
|
+
image: axllent/mailpit:latest
|
|
119
|
+
ports:
|
|
120
|
+
- "8025:8025"
|
|
121
|
+
- "1025:1025"
|
|
122
|
+
|
|
123
|
+
volumes:
|
|
124
|
+
postgres-data:
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Dockerfile - multi-stage build
|
|
128
|
+
|
|
129
|
+
```dockerfile
|
|
130
|
+
# Stage 1: Frontend build
|
|
131
|
+
FROM node:20-alpine AS frontend
|
|
132
|
+
WORKDIR /app
|
|
133
|
+
COPY package*.json ./
|
|
134
|
+
RUN npm ci
|
|
135
|
+
COPY resources/ resources/
|
|
136
|
+
COPY vite.config.ts tsconfig.json ./
|
|
137
|
+
RUN npm run build
|
|
138
|
+
|
|
139
|
+
# Stage 2: PHP application
|
|
140
|
+
FROM php:8.3-apache
|
|
141
|
+
WORKDIR /var/www/html
|
|
142
|
+
|
|
143
|
+
# System dependencies
|
|
144
|
+
RUN apt-get update && apt-get install -y \
|
|
145
|
+
libpq-dev libzip-dev zip unzip git \
|
|
146
|
+
&& docker-php-ext-install pdo_pgsql zip opcache
|
|
147
|
+
|
|
148
|
+
# Composer
|
|
149
|
+
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
|
150
|
+
|
|
151
|
+
# Application
|
|
152
|
+
COPY . .
|
|
153
|
+
COPY --from=frontend /app/public/build public/build
|
|
154
|
+
|
|
155
|
+
RUN composer install --optimize-autoloader --no-dev \
|
|
156
|
+
&& php artisan storage:link \
|
|
157
|
+
&& chown -R www-data:www-data storage bootstrap/cache
|
|
158
|
+
|
|
159
|
+
EXPOSE 80
|
|
160
|
+
CMD ["apache2-foreground"]
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Debugowanie
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
# Logi aplikacji (dzienne pliki!)
|
|
167
|
+
docker exec {{DOCKER_CONTAINER}} tail -f storage/logs/laravel-$(date +%Y-%m-%d).log
|
|
168
|
+
|
|
169
|
+
# Logi kontenera
|
|
170
|
+
docker compose logs -f app
|
|
171
|
+
|
|
172
|
+
# Status Horizon (kolejki)
|
|
173
|
+
docker exec {{DOCKER_CONTAINER}} php artisan horizon:status
|
|
174
|
+
|
|
175
|
+
# Sprawdz konfiguracje
|
|
176
|
+
docker exec {{DOCKER_CONTAINER}} php artisan config:show database
|
|
177
|
+
docker exec {{DOCKER_CONTAINER}} php artisan route:list --path=api/feature
|
|
178
|
+
|
|
179
|
+
# Czyszczenie po zmianach
|
|
180
|
+
docker exec {{DOCKER_CONTAINER}} php artisan optimize:clear
|
|
181
|
+
docker exec {{DOCKER_CONTAINER}} composer dump-autoload
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Zasady
|
|
185
|
+
|
|
186
|
+
1. **Zawsze Docker** - komendy PHP/Artisan/Composer w kontenerze, nigdy na hoscie
|
|
187
|
+
2. **Pelna nazwa kontenera** - `{{DOCKER_CONTAINER}}`, nie `docker compose exec app`
|
|
188
|
+
3. **Logi dzienne** - `laravel-YYYY-MM-DD.log`, nie `laravel.log`
|
|
189
|
+
4. **`optimize:clear`** - po zmianach konfiguracji, routow, zmiennych
|
|
190
|
+
5. **Multi-stage build** - frontend oddzielnie, PHP oddzielnie
|
|
191
|
+
6. **Volumes** - kod montowany przez volume w dev, kopiowany w prod
|
|
192
|
+
7. **Horizon** - oddzielny kontener dla kolejek, `restart: unless-stopped`
|
|
193
|
+
8. **Env** - `${VAR:-default}` w docker-compose, `.env` dla Laravel
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "**/*Resource*.php,**/*Request*.php,**/*Policy*.php,**/*Filter*.php"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# API Resources, Form Requests, Policies, Filters - Instrukcje
|
|
6
|
+
|
|
7
|
+
## API Resources (transformery)
|
|
8
|
+
|
|
9
|
+
Formatuja odpowiedz API - nigdy nie zwracaj modelu bezposrednio:
|
|
10
|
+
|
|
11
|
+
```php
|
|
12
|
+
declare(strict_types=1);
|
|
13
|
+
|
|
14
|
+
namespace Modules\Feature\Http\Resources;
|
|
15
|
+
|
|
16
|
+
use Illuminate\Http\Resources\Json\JsonResource;
|
|
17
|
+
|
|
18
|
+
class FeatureResource extends JsonResource
|
|
19
|
+
{
|
|
20
|
+
public function toArray($request): array
|
|
21
|
+
{
|
|
22
|
+
return [
|
|
23
|
+
'id' => $this->getId(),
|
|
24
|
+
'name' => $this->getName(),
|
|
25
|
+
'description' => $this->getDescription(),
|
|
26
|
+
'status' => $this->getStatus()->value,
|
|
27
|
+
'createdAt' => $this->getCreatedAt()->toIso8601String(),
|
|
28
|
+
'author' => UserResource::make($this->whenLoaded(self::RELATION_AUTHOR)),
|
|
29
|
+
'category' => CategoryResource::make($this->whenLoaded(self::RELATION_CATEGORY)),
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Collection Resource
|
|
36
|
+
|
|
37
|
+
```php
|
|
38
|
+
class FeatureCollection extends ResourceCollection
|
|
39
|
+
{
|
|
40
|
+
public $collects = FeatureResource::class;
|
|
41
|
+
|
|
42
|
+
public function toArray($request): array
|
|
43
|
+
{
|
|
44
|
+
return [
|
|
45
|
+
'data' => $this->collection,
|
|
46
|
+
'meta' => [
|
|
47
|
+
'total' => $this->total(),
|
|
48
|
+
'perPage' => $this->perPage(),
|
|
49
|
+
'currentPage' => $this->currentPage(),
|
|
50
|
+
],
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Form Requests (walidacja)
|
|
57
|
+
|
|
58
|
+
Kazdy endpoint z danymi wejsciowymi ma Form Request:
|
|
59
|
+
|
|
60
|
+
```php
|
|
61
|
+
declare(strict_types=1);
|
|
62
|
+
|
|
63
|
+
namespace Modules\Feature\Http\Requests;
|
|
64
|
+
|
|
65
|
+
use Illuminate\Foundation\Http\FormRequest;
|
|
66
|
+
use Illuminate\Validation\Rule;
|
|
67
|
+
|
|
68
|
+
class StoreFeatureRequest extends FormRequest
|
|
69
|
+
{
|
|
70
|
+
public function authorize(): bool
|
|
71
|
+
{
|
|
72
|
+
return $this->user()->can('create', Feature::class);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public function rules(): array
|
|
76
|
+
{
|
|
77
|
+
return [
|
|
78
|
+
'name' => ['required', 'string', 'min:2', 'max:255'],
|
|
79
|
+
'description' => ['nullable', 'string', 'max:5000'],
|
|
80
|
+
'category_id' => ['nullable', 'integer', 'exists:categories,id'],
|
|
81
|
+
'status' => ['required', Rule::in(FeatureStatus::cases())],
|
|
82
|
+
'tags' => ['nullable', 'array'],
|
|
83
|
+
'tags.*' => ['string', 'max:50'],
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public function messages(): array
|
|
88
|
+
{
|
|
89
|
+
return [
|
|
90
|
+
'name.required' => __('feature.validation.name_required'),
|
|
91
|
+
'name.max' => __('feature.validation.name_max'),
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Index/List Request z filtrowaniem i sortowaniem
|
|
98
|
+
|
|
99
|
+
```php
|
|
100
|
+
class IndexFeatureRequest extends FormRequest
|
|
101
|
+
{
|
|
102
|
+
public function rules(): array
|
|
103
|
+
{
|
|
104
|
+
return [
|
|
105
|
+
'filter' => ['nullable', 'array'],
|
|
106
|
+
'filter.name' => ['nullable', 'string'],
|
|
107
|
+
'filter.status' => ['nullable', Rule::in(FeatureStatus::cases())],
|
|
108
|
+
'filter.dateFrom' => ['nullable', 'date'],
|
|
109
|
+
'filter.dateTo' => ['nullable', 'date', 'after_or_equal:filter.dateFrom'],
|
|
110
|
+
'sort' => ['nullable', 'array'],
|
|
111
|
+
'sort.*' => ['string', Rule::in([
|
|
112
|
+
'name', '-name',
|
|
113
|
+
'createdAt', '-createdAt',
|
|
114
|
+
'status', '-status',
|
|
115
|
+
])],
|
|
116
|
+
'page' => ['required', 'integer', 'min:1'],
|
|
117
|
+
'limit' => ['required', 'integer', 'min:1', 'max:100'],
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Policies (autoryzacja)
|
|
124
|
+
|
|
125
|
+
```php
|
|
126
|
+
declare(strict_types=1);
|
|
127
|
+
|
|
128
|
+
namespace Modules\Feature\Http\Policies;
|
|
129
|
+
|
|
130
|
+
use App\Models\User;
|
|
131
|
+
|
|
132
|
+
class FeaturePolicy extends BasePolicy
|
|
133
|
+
{
|
|
134
|
+
public const MODULE_PERMISSION = Permission::FEATURES;
|
|
135
|
+
|
|
136
|
+
public function viewAny(User $user): bool
|
|
137
|
+
{
|
|
138
|
+
return $user->hasPermission(self::MODULE_PERMISSION);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
public function view(User $user, Feature $feature): bool
|
|
142
|
+
{
|
|
143
|
+
return $this->viewAny($user)
|
|
144
|
+
&& $feature->getUserId() === $user->getId();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
public function create(User $user): bool
|
|
148
|
+
{
|
|
149
|
+
return $user->hasPermission(self::MODULE_PERMISSION);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
public function update(User $user, Feature $feature): bool
|
|
153
|
+
{
|
|
154
|
+
return $this->admin($user)
|
|
155
|
+
|| $feature->getUserId() === $user->getId();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
public function delete(User $user, Feature $feature): bool
|
|
159
|
+
{
|
|
160
|
+
return $this->admin($user);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
public function admin(User $user): bool
|
|
164
|
+
{
|
|
165
|
+
return $this->moduleAdmin($user);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Uzycie w kontrolerze
|
|
171
|
+
|
|
172
|
+
```php
|
|
173
|
+
// W kontrolerze - authorize przez can()
|
|
174
|
+
public function update(UpdateFeatureRequest $request, int $featureId): JsonResponse
|
|
175
|
+
{
|
|
176
|
+
$feature = $this->service->findOrFail($featureId);
|
|
177
|
+
$this->authorize('update', $feature);
|
|
178
|
+
|
|
179
|
+
// ...
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// W routach
|
|
183
|
+
Route::get('/features', [FeatureController::class, 'index'])
|
|
184
|
+
->can('viewAny', Feature::class);
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Custom Query Filters (Spatie QueryBuilder)
|
|
188
|
+
|
|
189
|
+
```php
|
|
190
|
+
declare(strict_types=1);
|
|
191
|
+
|
|
192
|
+
namespace Modules\Feature\Database\Filters;
|
|
193
|
+
|
|
194
|
+
use Spatie\QueryBuilder\Filters\Filter;
|
|
195
|
+
use Illuminate\Database\Eloquent\Builder;
|
|
196
|
+
|
|
197
|
+
class DateRangeFilter implements Filter
|
|
198
|
+
{
|
|
199
|
+
public function __construct(
|
|
200
|
+
private readonly string $column,
|
|
201
|
+
) {}
|
|
202
|
+
|
|
203
|
+
public function __invoke(Builder $query, $value, string $property): void
|
|
204
|
+
{
|
|
205
|
+
if (is_array($value)) {
|
|
206
|
+
if (isset($value['from'])) {
|
|
207
|
+
$query->where($this->column, '>=', Carbon::parse($value['from']));
|
|
208
|
+
}
|
|
209
|
+
if (isset($value['to'])) {
|
|
210
|
+
$query->where($this->column, '<=', Carbon::parse($value['to']));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Rejestracja w modelu
|
|
218
|
+
|
|
219
|
+
```php
|
|
220
|
+
class FeatureModel extends Model
|
|
221
|
+
{
|
|
222
|
+
public static function allowedFilters(): array
|
|
223
|
+
{
|
|
224
|
+
return [
|
|
225
|
+
AllowedFilter::exact('status', self::COLUMN_STATUS),
|
|
226
|
+
AllowedFilter::partial('name', self::COLUMN_NAME),
|
|
227
|
+
AllowedFilter::custom('date', new DateRangeFilter(self::COLUMN_CREATED_AT)),
|
|
228
|
+
AllowedFilter::exact('category', self::RELATION_CATEGORY . '.id'),
|
|
229
|
+
];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
public static function allowedSorts(): array
|
|
233
|
+
{
|
|
234
|
+
return [
|
|
235
|
+
AllowedSort::field('name', self::COLUMN_NAME),
|
|
236
|
+
AllowedSort::field('createdAt', self::COLUMN_CREATED_AT),
|
|
237
|
+
];
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Zasady
|
|
243
|
+
|
|
244
|
+
1. **Resource** - nigdy nie zwracaj modelu, zawsze Resource
|
|
245
|
+
2. **`whenLoaded()`** - dla relacji w Resource, zapobiega N+1
|
|
246
|
+
3. **Form Request** - `authorize()` + `rules()`, waliduj filtry i sortowania
|
|
247
|
+
4. **Policy** - CRUD nazwy (`view`, `viewAny`, `create`, `update`, `delete`)
|
|
248
|
+
5. **`->can()`** - uzyj w routach lub `$this->authorize()` w kontrolerze
|
|
249
|
+
6. **Filters** - custom filtr implementuje `Spatie\QueryBuilder\Filters\Filter`
|
|
250
|
+
7. **Kolumny jako stale** - `self::COLUMN_NAME`, nie magic strings
|
|
251
|
+
8. **Tlumaczenia** - `messages()` w Form Request uzywaja `__('klucz')`
|