From 65ef61e8c22142aec25dda427fb4623eefd4cbf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Metin=20=C5=9Eim=C5=9Fek?= <75851971+devsimsek@users.noreply.github.com> Date: Fri, 12 Jun 2026 13:48:52 +0200 Subject: [PATCH 1/2] feat(cli): add agent install command; move skill to wiki with YAML frontmatter --- .gitignore | 3 + sdf/cli | 73 ++++++ tests/CLIGeneratorTest.php | 32 +++ wiki/agentic_workflow/skills/project-sdf.md | 208 ------------------ .../skills/project-sdf/SKILL.md | 146 ++++++++++++ 5 files changed, 254 insertions(+), 208 deletions(-) delete mode 100644 wiki/agentic_workflow/skills/project-sdf.md create mode 100644 wiki/agentic_workflow/skills/project-sdf/SKILL.md diff --git a/.gitignore b/.gitignore index 4f599a3..62ec72c 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,6 @@ app/cache/* *.cache *.tmp .phpactor.json + +# generated by php sdf/cli agent install +.agents/ diff --git a/sdf/cli b/sdf/cli index 6f416fb..359d396 100755 --- a/sdf/cli +++ b/sdf/cli @@ -94,6 +94,9 @@ class CLI case "swagger": $this->handleSwagger(); break; + case "agent": + $this->handleAgent(); + break; default: $this->printUsage(); break; @@ -1448,6 +1451,74 @@ class CLI return !empty(shell_exec("lsof -i:$port")); } + /** Install agent skill to local or global agents directory. */ + private function handleAgent(): void + { + $sub = $this->argv[2] ?? null; + + if ($sub !== 'install') { + echo "Usage: php sdf/cli agent install [local|global]\n"; + echo " install local (default) Copy skill to .agents/skills//SKILL.md\n"; + echo " install global Copy skill to ~/.agents/skills//SKILL.md\n"; + exit(1); + } + + $scope = $this->argv[3] ?? 'local'; + + if ($scope !== 'local' && $scope !== 'global') { + echo "Usage: php sdf/cli agent install [local|global]\n"; + echo " install local (default) Copy skill to .agents/skills//SKILL.md\n"; + echo " install global Copy skill to ~/.agents/skills//SKILL.md\n"; + exit(1); + } + + // Determine source (project's wiki/agentic_workflow/skills/) + $projectRoot = dirname(__DIR__); + $skillDir = ($scope === 'global') + ? getenv('HOME') . '/.agents/skills' + : getcwd() . '/.agents/skills'; + + // Find all skill sources in the project wiki + $sourceDir = $projectRoot . '/wiki/agentic_workflow/skills'; + if (!is_dir($sourceDir)) { + echo "No skills found at $sourceDir. Create a .agents/skills//SKILL.md first.\n"; + exit(1); + } + + $installed = 0; + $items = scandir($sourceDir); + foreach ($items as $item) { + if ($item === '.' || $item === '..') continue; + $skillPath = $sourceDir . '/' . $item . '/SKILL.md'; + if (!is_file($skillPath)) continue; + + $destDir = $skillDir . '/' . $item; + $destFile = $destDir . '/SKILL.md'; + + // Skip if copying to the same path + if (realpath($skillPath) === realpath($destFile)) { + echo "Skill '$item' already in place at $destFile\n"; + $installed++; + continue; + } + + @mkdir($destDir, 0755, true); + if (copy($skillPath, $destFile)) { + echo "Installed skill '$item' to $destFile\n"; + $installed++; + } else { + echo "Failed to install skill '$item' to $destFile\n"; + } + } + + if ($installed === 0) { + echo "No SKILL.md files found under $sourceDir\n"; + exit(1); + } + + echo "\nDone. $installed skill(s) installed.\n"; + } + /** Print banner. */ private function showBanner(): void { @@ -1494,6 +1565,8 @@ class CLI echo "\n"; echo " swagger [action] OpenAPI / Swagger documentation\n"; echo " generate [output-path] Generate openapi.json from controllers\n"; + echo "\n"; + echo " agent install [local|global] Install agent skill (default: local)\n"; } } diff --git a/tests/CLIGeneratorTest.php b/tests/CLIGeneratorTest.php index 69c5163..93176f3 100644 --- a/tests/CLIGeneratorTest.php +++ b/tests/CLIGeneratorTest.php @@ -26,6 +26,38 @@ protected function tearDown(): void if (is_dir($this->tmpDir)) rmdir($this->tmpDir); } + public function test_agent_install_local(): void + { + $script = escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg(getcwd() . '/sdf/cli'); + $cmd = "cd " . escapeshellarg($this->tmpDir) . " && $script agent install"; + exec($cmd, $out, $exit); + $this->assertSame(0, $exit, "CLI exited non-zero: " . implode("\n", $out)); + $this->assertFileExists($this->tmpDir . '/.agents/skills/project-sdf/SKILL.md'); + $content = file_get_contents($this->tmpDir . '/.agents/skills/project-sdf/SKILL.md'); + $this->assertStringContainsString('name: project-sdf', $content); + } + + public function test_agent_install_global(): void + { + $script = escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg(getcwd() . '/sdf/cli'); + $globalDir = sys_get_temp_dir() . '/sdf_agent_global_' . uniqid(); + putenv('HOME=' . $globalDir); + $cmd = "$script agent install global"; + exec($cmd, $out, $exit); + $this->assertSame(0, $exit, "CLI exited non-zero: " . implode("\n", $out)); + $this->assertFileExists($globalDir . '/.agents/skills/project-sdf/SKILL.md'); + putenv('HOME'); + } + + public function test_agent_install_requires_subcommand(): void + { + $script = escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg(getcwd() . '/sdf/cli'); + $cmd = "cd " . escapeshellarg($this->tmpDir) . " && $script agent"; + exec($cmd, $out, $exit); + $this->assertNotSame(0, $exit); + $this->assertStringContainsString('Usage', implode('', $out)); + } + public function test_generate_unit_test_file(): void { $script = escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg(getcwd() . '/sdf/cli'); diff --git a/wiki/agentic_workflow/skills/project-sdf.md b/wiki/agentic_workflow/skills/project-sdf.md deleted file mode 100644 index 9a14ba2..0000000 --- a/wiki/agentic_workflow/skills/project-sdf.md +++ /dev/null @@ -1,208 +0,0 @@ -# Project SDF — Agent Skill - -## Quick start -```bash -composer install -php sdf/cli serve -p 8000 # dev server at localhost:8000 -php sdf/cli serve -p 8000 --live # with auto-reload -php sdf/cli serve -p 8000 --clear-cache # clear caches before serving -``` - -## CLI reference -```bash -php sdf/cli g controller Home # generate a controller -php sdf/cli g model Post # generate a model -php sdf/cli g migration create_posts_table # generate a migration -php sdf/cli g test HomeController --type=controller --namespace=App\\Controllers\\User -php sdf/cli db migrate # run pending migrations -php sdf/cli cache clear # flush all caches -php sdf/cli format --dry-run -v # check code style -php sdf/cli swagger generate # generate OpenAPI spec -``` - -## Project structure -``` -├── app/ -│ ├── config/ # PHP/JSON config files → $config['key'] = [...] -│ ├── controllers/ # your controllers -│ ├── handlers/ # error handlers (eh_pathNotFound, eh_methodNotAllowed) -│ ├── models/ # your models -│ └── views/ # Fuse templates (.fuse.php) -├── sdf/ # framework core (don't touch) -├── public/ # static assets (images, css, js) -├── storage/ # logs, cache, uploads -├── tests/ -├── vendor/ -├── .env # environment variables -├── index.php # entry point (defines SDF, SDF_ENV, etc.) -└── routes.php # route definitions -``` - -## Routing (`routes.php`) -```php -// Closure -Router::add('/', function() { return 'Hello'; }); - -// Controller@method (namespaced) -Router::add('/users', '\App\Controllers\UserController@index'); - -// With params -Router::add('/post/{id}', '\App\Controllers\PostController@show'); - -// Route placeholders: {id}, {uuid}, {slug}, {date}, {all}, etc. -Router::add('/user/{uuid}', 'UserController@show'); -Router::add('/blog/{slug}', 'BlogController@show'); - -// HTTP method filter -Router::add('/users', 'UserController@store', 'POST'); - -// Named routes -Router::add('/login', 'AuthController@login', 'GET')->name('login'); -``` - -See [full route docs](wiki/app/routes.md) for all 18 available placeholders (`{uuid}`, `{hex}`, `{alpha}`, `{bool}`, `{datetime}`, etc.). - -## Controllers -```php -namespace App\Controllers; -class HomeController { - public function index() { - return view('home', ['title' => 'SDF']); - } -} -``` - -## Views (Fuse — Blade-like syntax) -```php - - - @yield('content') - - - -@extends('layouts.app') -@section('content') -

{{ $title }}

- @include('partials.nav') -@endsection -``` - -## Database (Spark ORM) -### Config (`app/config/database.php`) -```php -$config['database'] = [ - 'driver' => 'mysql', - 'host' => getenv('DB_HOST') ?: '127.0.0.1', - 'database' => getenv('DB_NAME'), - 'username' => getenv('DB_USER'), - 'password' => getenv('DB_PASS'), - 'charset' => 'utf8mb4', -]; -``` - -### Query builder -```php -$users = Spark::table('users')->where('active', 1)->get(); -$user = Spark::table('users')->find(1); -$posts = Spark::table('posts')->where('user_id', $id)->paginate(15); -``` - -### Models -```php -class Post extends \SDF\Spark\Model { - protected string $table = 'posts'; - protected string $primaryKey = 'id'; - protected bool $timestamps = true; // adds created_at/updated_at - protected bool $softDelete = false; // adds deleted_at - - // Relationships - public function author() { - return $this->belongsTo(User::class, 'user_id'); - } - public function comments() { - return $this->hasMany(Comment::class, 'post_id'); - } - public function tags() { - return $this->belongsToMany(Tag::class, 'post_tag', 'post_id', 'tag_id'); - } -} - -// Usage -$post = Post::find(1); -$post->title = 'Updated'; -$post->save(); -$post->delete(); // soft-delete if enabled -$post->forceDelete(); // permanent delete -Post::withTrashed()->get(); -Post::onlyTrashed()->get(); -``` - -## Authentication -### Guard types -- **SessionGuard** — session-based login. Configure in `app/config/auth.php`. -- **JwtGuard** — stateless HS256 JWT with refresh tokens. - -### Protecting routes -```php -Router::add('/dashboard', 'DashboardController@index')->middleware('auth'); -``` - -### Auth middleware stack -- `CsrfMiddleware` — validates `X-CSRF-TOKEN` or `_token` POST field. 419 on mismatch. -- `CorsMiddleware` — configurable origins/methods/headers. OPTIONS → 204. -- `RateLimitMiddleware` — 60 req/min per IP+route. 429 with `Retry-After`. -- `AuthMiddleware` — 401 if unauthenticated. - -## Cache (PSR-16) -```php -use SDF\Cache\Cache; - -Cache::set('key', 'value', 3600); -$value = Cache::get('key', 'default'); -Cache::delete('key'); -Cache::clear(); -``` - -Drivers: `file`, `redis`, `memcached`. Set in `app/config/cache.php`. - -## Validation -```php -$validator = new \SDF\Validation\Validator($data, [ - 'email' => 'required|email', - 'name' => 'required|min:3|max:255', - 'age' => 'numeric|between:18,99', -]); -if ($validator->fails()) { - $errors = $validator->errors(); -} -``` - -19 rules: required, email, min, max, between, numeric, integer, string, boolean, array, alpha, alpha_num, url, in, confirmed, same, different, regex, nullable. - -## Components -- **Encryption**: AES-256-CBC + HMAC-SHA256. `Encrypter::encrypt($plaintext)`, `decrypt($ciphertext)`. Key from `APP_KEY` env (32 bytes). -- **PSR-7 HTTP**: `ServerRequest` and `Response` — immutable (`with*` returns new instance). Legacy mutable classes also available. -- **Storage**: `Storage::disk('local')->put('file.txt', 'content')`. Drivers: `local`, `s3`. -- **Mail**: `Mail::send(...)` via configured driver. -- **Queues**: `Queue::dispatch(Job::class, $payload)` via `database` or `redis` driver. -- **Events**: PSR-14 `EventDispatcher::dispatch(new UserRegistered($user))`. -- **Logging**: PSR-3 `Logger::info('message')`, `Logger::error('message', $context)`. -- **Schema Builder**: `Schema::create('table', function($table) { ... })` for migrations. -- **Localization**: `__('messages.welcome')` with language files in `app/lang/`. - -## Deployment -### Docker -```bash -docker compose up -d # starts FrankenPHP + MySQL 8.4 + Redis 7 -``` - -Multi-stage Dockerfile: `base` (composer --no-dev), `dev` (xdebug), `production` (OPcache). Set `SDF_ENV=production` for production mode. - -### Static files -Served automatically in development mode. In production, use Caddy (included in Docker) or nginx to serve `public/`. - -## Wiki docs -Full documentation at `wiki/`: -- `wiki/libraries/` — component docs (auth, cache, csrf, cors, ratelimit, encryption, validation, session, flash, http, swagger, log, schema, events, localization, mail, queue, storage) -- `wiki/app/tutorials/` — tutorials (authentication, blog, docker-frankenphp) -- `wiki/sdf/` — CLI commands docs diff --git a/wiki/agentic_workflow/skills/project-sdf/SKILL.md b/wiki/agentic_workflow/skills/project-sdf/SKILL.md new file mode 100644 index 0000000..898a8b3 --- /dev/null +++ b/wiki/agentic_workflow/skills/project-sdf/SKILL.md @@ -0,0 +1,146 @@ +--- +name: project-sdf +description: Build and run SDF PHP projects — CLI, routing, Spark ORM, Fuse views, auth, cache, queues, mail, Docker deployment +license: MIT +compatibility: opencode +metadata: + audience: developers + framework: sdf +--- + +## Quick start +```bash +composer install +php sdf/cli serve -p 8000 # dev server at localhost:8000 +php sdf/cli serve -p 8000 --live # with auto-reload +php sdf/cli serve -p 8000 --clear-cache # clear caches before serving +``` + +## CLI reference +```bash +php sdf/cli g controller Home # generate a controller +php sdf/cli g model Post # generate a model +php sdf/cli g migration create_posts_table # generate a migration +php sdf/cli g test HomeController --type=controller --namespace=App\\Controllers\\User +php sdf/cli db migrate # run pending migrations +php sdf/cli cache clear # flush all caches +php sdf/cli format --dry-run -v # check code style +php sdf/cli agent install # install this skill locally +php sdf/cli agent install global # install this skill globally +``` + +## Project structure +``` +├── app/ +│ ├── config/ # PHP/JSON config files → $config['key'] = [...] +│ ├── controllers/ # your controllers +│ ├── handlers/ # error handlers +│ ├── models/ # your models +│ └── views/ # Fuse templates (.fuse.php) +├── sdf/ # framework core (don't touch) +├── public/ # static assets (images, css, js) +├── storage/ # logs, cache, uploads +├── tests/ +├── vendor/ +├── .env # environment variables +├── index.php # entry point +└── routes.php # route definitions +``` + +## Routing +```php +Router::add('/', function() { return 'Hello'; }); +Router::add('/users', '\App\Controllers\UserController@index'); +Router::add('/post/{id}', '\App\Controllers\PostController@show'); +Router::add('/user/{uuid}', 'UserController@show'); +Router::add('/login', 'AuthController@login', 'GET')->name('login'); +``` +Route placeholders: `{id}`, `{uuid}`, `{uuid_simple}`, `{slug}`, `{date}`, `{all}`, `{hex}`, `{alpha}`, `{alnum}`, `{word}`, `{segment}`, `{file}`, `{bool}`, `{time}`, `{datetime}`, `{url}`, `{num}`. + +## Controllers +```php +namespace App\Controllers; +class HomeController { + public function index() { + return view('home', ['title' => 'SDF']); + } +} +``` + +## Views (Fuse — Blade-like syntax) +```php +@extends('layouts.app') +@section('content') +

{{ $title }}

+ @include('partials.nav') +@endsection +``` + +## Database (Spark ORM) +```php +// Query builder +$users = Spark::table('users')->where('active', 1)->get(); +$user = Spark::table('users')->find(1); +$posts = Spark::table('posts')->where('user_id', $id)->paginate(15); + +// Models +class Post extends \SDF\Spark\Model { + protected string $table = 'posts'; + protected string $primaryKey = 'id'; + protected bool $timestamps = true; + protected bool $softDelete = false; + + public function author() { return $this->belongsTo(User::class, 'user_id'); } + public function comments() { return $this->hasMany(Comment::class, 'post_id'); } + public function tags() { return $this->belongsToMany(Tag::class, 'post_tag', 'post_id', 'tag_id'); } +} + +$post = Post::find(1); +$post->title = 'Updated'; +$post->save(); +``` + +## Authentication +- **SessionGuard** — session-based login +- **JwtGuard** — stateless HS256 JWT with refresh tokens +```php +Router::add('/dashboard', 'DashboardController@index')->middleware('auth'); +``` +Middleware stack: `CsrfMiddleware`, `CorsMiddleware`, `RateLimitMiddleware`, `AuthMiddleware`. + +## Cache (PSR-16) +```php +use SDF\Cache\Cache; +Cache::set('key', 'value', 3600); +$value = Cache::get('key', 'default'); +``` +Drivers: `file`, `redis`, `memcached`. + +## Validation +```php +$v = new \SDF\Validation\Validator($data, [ + 'email' => 'required|email', + 'name' => 'required|min:3|max:255', +]); +``` +19 rules: required, email, min, max, between, numeric, integer, string, boolean, array, alpha, alpha_num, url, in, confirmed, same, different, regex, nullable. + +## Components +- **Encryption**: AES-256-CBC + HMAC-SHA256 +- **PSR-7 HTTP**: immutable `ServerRequest` and `Response` +- **Storage**: `Storage::disk('local')->put('file.txt', 'content')` +- **Mail**: `Mail::send(...)` via configured driver +- **Queues**: `Queue::dispatch(Job::class, $payload)` +- **Events**: PSR-14 `EventDispatcher::dispatch(new UserRegistered($user))` +- **Logging**: PSR-3 `Logger::info('message')` +- **Schema Builder**: `Schema::create('table', function($table) { ... })` +- **Localization**: `__('messages.welcome')` + +## Deployment +```bash +docker compose up -d # starts FrankenPHP + MySQL 8.4 + Redis 7 +``` +Multi-stage Dockerfile, Caddyfile included. Set `SDF_ENV=production` for production mode. + +## When to use me +Use this skill when working on an SDF PHP project: scaffolding code, defining routes, building models and migrations, setting up authentication, managing caches, or deploying with Docker. Ask clarifying questions if the task is ambiguous. From df4aeed095616221051958ee1e969b4dee552052 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Metin=20=C5=9Eim=C5=9Fek?= <75851971+devsimsek@users.noreply.github.com> Date: Fri, 12 Jun 2026 13:50:32 +0200 Subject: [PATCH 2/2] feat(skill): add workflows, troubleshooting, quick reference tables --- .../skills/project-sdf/SKILL.md | 143 ++++++++++++++---- 1 file changed, 110 insertions(+), 33 deletions(-) diff --git a/wiki/agentic_workflow/skills/project-sdf/SKILL.md b/wiki/agentic_workflow/skills/project-sdf/SKILL.md index 898a8b3..1536fd5 100644 --- a/wiki/agentic_workflow/skills/project-sdf/SKILL.md +++ b/wiki/agentic_workflow/skills/project-sdf/SKILL.md @@ -8,23 +8,78 @@ metadata: framework: sdf --- +## What I do + +I help you build, scaffold, and maintain SDF PHP framework projects. I know the full stack: CLI generators, routing, Spark ORM models/migrations, Fuse templates, session/JWT auth, PSR-16 cache, mail, queues, events, schema builder, and Docker deployment. + +Invoke me with `/project-sdf` followed by what you need. + +## When to use me + +| Scenario | What I'll do | +|----------|-------------| +| Scaffold a new feature | Generate controller, model, migration, view, routes, and test | +| Fix a bug | Diagnose the issue, suggest or apply the fix, run tests | +| Add an API endpoint | Create controller + route + validation + test | +| Database changes | Generate and run migrations, update models | +| Docker/deploy | Review compose.yaml, Caddyfile, Dockerfile, suggest improvements | +| Code review | Inspect changed files with security in mind, enforce SDF conventions | +| Run the app | Start dev server, run tests, check code style | +| Install the skill | `php sdf/cli agent install [local|global]` | + ## Quick start ```bash composer install php sdf/cli serve -p 8000 # dev server at localhost:8000 php sdf/cli serve -p 8000 --live # with auto-reload -php sdf/cli serve -p 8000 --clear-cache # clear caches before serving +``` + +## Common workflows + +### Scaffold a full CRUD +```bash +php sdf/cli g model Post +php sdf/cli g migration create_posts_table +php sdf/cli g controller PostController +php sdf/cli g test PostControllerTest --type=controller +``` +Then add routes in `routes.php`, fill in controller methods, define the migration schema with `Schema::create()`, and add Fuse views in `app/views/`. + +### Add auth to a route +```php +Router::add('/dashboard', 'DashboardController@index')->middleware('auth'); +``` +Guards: `SessionGuard` (session-based), `JwtGuard` (HS256 JWT with refresh tokens). + +### Fix code style + run checks +```bash +php sdf/cli format --dry-run -v # preview style issues +php sdf/cli fmt # auto-fix +vendor/bin/phpunit # run tests +composer analyze # PHPStan level 5 +``` + +### Spin up full stack +```bash +docker compose up -d # FrankenPHP + MySQL 8.4 + Redis 7 ``` ## CLI reference ```bash php sdf/cli g controller Home # generate a controller php sdf/cli g model Post # generate a model -php sdf/cli g migration create_posts_table # generate a migration +php sdf/cli g migration create_posts_table php sdf/cli g test HomeController --type=controller --namespace=App\\Controllers\\User +php sdf/cli g middleware Auth +php sdf/cli g guard Api +php sdf/cli g seeder UserSeeder php sdf/cli db migrate # run pending migrations +php sdf/cli db rollback # rollback last batch +php sdf/cli db seed # run all seeders +php sdf/cli db reset # rollback all + migrate + seed php sdf/cli cache clear # flush all caches php sdf/cli format --dry-run -v # check code style +php sdf/cli swagger generate # generate OpenAPI spec php sdf/cli agent install # install this skill locally php sdf/cli agent install global # install this skill globally ``` @@ -34,17 +89,20 @@ php sdf/cli agent install global # install this skill globally ├── app/ │ ├── config/ # PHP/JSON config files → $config['key'] = [...] │ ├── controllers/ # your controllers -│ ├── handlers/ # error handlers +│ ├── handlers/ # error handlers (eh_pathNotFound, etc.) │ ├── models/ # your models │ └── views/ # Fuse templates (.fuse.php) ├── sdf/ # framework core (don't touch) -├── public/ # static assets (images, css, js) +├── public/ # static assets ├── storage/ # logs, cache, uploads ├── tests/ ├── vendor/ -├── .env # environment variables +├── .env # APP_KEY, DB_*, etc. ├── index.php # entry point -└── routes.php # route definitions +├── routes.php # route definitions +├── Dockerfile # multi-stage (base/dev/production) +├── compose.yaml # app + MySQL 8.4 + Redis 7 +└── Caddyfile # FrankenPHP/Caddy config ``` ## Routing @@ -54,16 +112,19 @@ Router::add('/users', '\App\Controllers\UserController@index'); Router::add('/post/{id}', '\App\Controllers\PostController@show'); Router::add('/user/{uuid}', 'UserController@show'); Router::add('/login', 'AuthController@login', 'GET')->name('login'); +Router::add('/dashboard', 'DashboardController@index')->middleware('auth'); ``` -Route placeholders: `{id}`, `{uuid}`, `{uuid_simple}`, `{slug}`, `{date}`, `{all}`, `{hex}`, `{alpha}`, `{alnum}`, `{word}`, `{segment}`, `{file}`, `{bool}`, `{time}`, `{datetime}`, `{url}`, `{num}`. +Available placeholders: `{id}`, `{uuid}`, `{uuid_simple}`, `{slug}`, `{date}`, `{all}`, `{hex}`, `{alpha}`, `{alnum}`, `{word}`, `{segment}`, `{file}`, `{bool}`, `{time}`, `{datetime}`, `{url}`, `{num}`. ## Controllers ```php namespace App\Controllers; -class HomeController { - public function index() { - return view('home', ['title' => 'SDF']); - } +class PostController { + public function index() { /* list */ } + public function show(int $id) { /* single */ } + public function store() { /* create */ } + public function update(int $id) { /* update */ } + public function destroy(int $id) { /* delete */ } } ``` @@ -81,17 +142,18 @@ class HomeController { // Query builder $users = Spark::table('users')->where('active', 1)->get(); $user = Spark::table('users')->find(1); -$posts = Spark::table('posts')->where('user_id', $id)->paginate(15); +$posts = Spark::table('users')->where('age', '>', 18)->orderBy('name')->paginate(15); -// Models +// Mass assignment protection class Post extends \SDF\Spark\Model { protected string $table = 'posts'; protected string $primaryKey = 'id'; protected bool $timestamps = true; protected bool $softDelete = false; + protected array $fillable = ['title', 'content', 'published_at']; + // or: protected array $guarded = ['id', 'is_admin']; public function author() { return $this->belongsTo(User::class, 'user_id'); } - public function comments() { return $this->hasMany(Comment::class, 'post_id'); } public function tags() { return $this->belongsToMany(Tag::class, 'post_tag', 'post_id', 'tag_id'); } } @@ -101,46 +163,61 @@ $post->save(); ``` ## Authentication -- **SessionGuard** — session-based login -- **JwtGuard** — stateless HS256 JWT with refresh tokens +- **SessionGuard** — session-based login. Configure in `app/config/auth.php` +- **JwtGuard** — stateless HS256 JWT with refresh tokens. Min 32-byte key ```php Router::add('/dashboard', 'DashboardController@index')->middleware('auth'); ``` -Middleware stack: `CsrfMiddleware`, `CorsMiddleware`, `RateLimitMiddleware`, `AuthMiddleware`. +Middleware stack (applied in order): `CsrfMiddleware`, `CorsMiddleware`, `RateLimitMiddleware` (60 req/min), `AuthMiddleware`. ## Cache (PSR-16) ```php use SDF\Cache\Cache; Cache::set('key', 'value', 3600); $value = Cache::get('key', 'default'); +Cache::delete('key'); +Cache::clear(); ``` -Drivers: `file`, `redis`, `memcached`. +Drivers: `file` (default), `redis`, `memcached`. Set in `app/config/cache.php`. ## Validation ```php $v = new \SDF\Validation\Validator($data, [ 'email' => 'required|email', 'name' => 'required|min:3|max:255', + 'age' => 'numeric|between:18,99', ]); +if ($v->fails()) { $errors = $v->errors(); } ``` 19 rules: required, email, min, max, between, numeric, integer, string, boolean, array, alpha, alpha_num, url, in, confirmed, same, different, regex, nullable. -## Components -- **Encryption**: AES-256-CBC + HMAC-SHA256 -- **PSR-7 HTTP**: immutable `ServerRequest` and `Response` -- **Storage**: `Storage::disk('local')->put('file.txt', 'content')` -- **Mail**: `Mail::send(...)` via configured driver -- **Queues**: `Queue::dispatch(Job::class, $payload)` -- **Events**: PSR-14 `EventDispatcher::dispatch(new UserRegistered($user))` -- **Logging**: PSR-3 `Logger::info('message')` -- **Schema Builder**: `Schema::create('table', function($table) { ... })` -- **Localization**: `__('messages.welcome')` +## Components quick ref + +| Component | Key class / method | +|-----------|-------------------| +| Encryption | `Encrypter::encrypt($text)` / `decrypt($cipher)` — AES-256-CBC + HMAC-SHA256 | +| PSR-7 HTTP | `ServerRequest`, `Response` — immutable (`with*` returns new instance) | +| Storage | `Storage::disk('local')->put('file.txt', 'content')` — drivers: local, s3 | +| Mail | `Mail::send($to, $subject, $body)` — via `NativeMailer` or SMTP | +| Queues | `Queue::dispatch(Job::class, $payload)` — drivers: `database`, `redis` | +| Events | `EventDispatcher::dispatch(new UserRegistered($user))` — PSR-14 | +| Logging | `Logger::info('msg')`, `Logger::error('msg', $context)` — PSR-3 | +| Schema | `Schema::create('table', fn($t) => $t->increments('id')...)` | +| Localization | `__('messages.welcome')` — language files in `app/lang/` | ## Deployment ```bash -docker compose up -d # starts FrankenPHP + MySQL 8.4 + Redis 7 +docker compose up -d # FrankenPHP + MySQL 8.4 + Redis 7 ``` -Multi-stage Dockerfile, Caddyfile included. Set `SDF_ENV=production` for production mode. - -## When to use me -Use this skill when working on an SDF PHP project: scaffolding code, defining routes, building models and migrations, setting up authentication, managing caches, or deploying with Docker. Ask clarifying questions if the task is ambiguous. +- `Dockerfile` multi-stage: `base` (composer deps), `dev` (xdebug), `production` (OPcache) +- Set `SDF_ENV=production` for production mode +- Static files served from `public/` (Caddy in Docker, dev server in dev mode) +- Caddyfile includes security headers, gzip, forbidden paths + +## Troubleshooting +- **500 error / blank page**: Check `storage/logs/` for framework logs. Enable `SDF_ENV=development` for detailed errors +- **419 page expired**: CSRF token mismatch — ensure `X-CSRF-TOKEN` header or `_token` POST field is sent +- **429 too many requests**: Rate limit hit (default 60 req/min per IP+route). Wait for `Retry-After` header +- **Cache not updating**: Run `php sdf/cli cache clear` to flush all caches. Or set `debug = true` in router config to disable route cache +- **Migration fails**: Run `php sdf/cli db reset` to rollback all + re-migrate. Check `database.php` config for correct credentials +- **Docker connection refused**: Containers may still be starting. Run `docker compose logs` to check each service