update deploy

This commit is contained in:
bulut
2026-03-27 10:41:54 +03:00
parent 69d19c0176
commit 6f6448aa06
422 changed files with 37956 additions and 0 deletions

View File

@@ -0,0 +1,167 @@
---
name: pest-testing
description: "Tests applications using the Pest 4 PHP framework. Activates when writing tests, creating unit or feature tests, adding assertions, testing Livewire components, browser testing, debugging test failures, working with datasets or mocking; or when the user mentions test, spec, TDD, expects, assertion, coverage, or needs to verify functionality works."
license: MIT
metadata:
author: laravel
---
# Pest Testing 4
## When to Apply
Activate this skill when:
- Creating new tests (unit, feature, or browser)
- Modifying existing tests
- Debugging test failures
- Working with browser testing or smoke testing
- Writing architecture tests or visual regression tests
## Documentation
Use `search-docs` for detailed Pest 4 patterns and documentation.
## Basic Usage
### Creating Tests
All tests must be written using Pest. Use `php artisan make:test --pest {name}`.
### Test Organization
- Unit/Feature tests: `tests/Feature` and `tests/Unit` directories.
- Browser tests: `tests/Browser/` directory.
- Do NOT remove tests without approval - these are core application code.
### Basic Test Structure
<!-- Basic Pest Test Example -->
```php
it('is true', function () {
expect(true)->toBeTrue();
});
```
### Running Tests
- Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`.
- Run all tests: `php artisan test --compact`.
- Run file: `php artisan test --compact tests/Feature/ExampleTest.php`.
## Assertions
Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`:
<!-- Pest Response Assertion -->
```php
it('returns all', function () {
$this->postJson('/api/docs', [])->assertSuccessful();
});
```
| Use | Instead of |
|-----|------------|
| `assertSuccessful()` | `assertStatus(200)` |
| `assertNotFound()` | `assertStatus(404)` |
| `assertForbidden()` | `assertStatus(403)` |
## Mocking
Import mock function before use: `use function Pest\Laravel\mock;`
## Datasets
Use datasets for repetitive tests (validation rules, etc.):
<!-- Pest Dataset Example -->
```php
it('has emails', function (string $email) {
expect($email)->not->toBeEmpty();
})->with([
'james' => 'james@laravel.com',
'taylor' => 'taylor@laravel.com',
]);
```
## Pest 4 Features
| Feature | Purpose |
|---------|---------|
| Browser Testing | Full integration tests in real browsers |
| Smoke Testing | Validate multiple pages quickly |
| Visual Regression | Compare screenshots for visual changes |
| Test Sharding | Parallel CI runs |
| Architecture Testing | Enforce code conventions |
### Browser Test Example
Browser tests run in real browsers for full integration testing:
- Browser tests live in `tests/Browser/`.
- Use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories.
- Use `RefreshDatabase` for clean state per test.
- Interact with page: click, type, scroll, select, submit, drag-and-drop, touch gestures.
- Test on multiple browsers (Chrome, Firefox, Safari) if requested.
- Test on different devices/viewports (iPhone 14 Pro, tablets) if requested.
- Switch color schemes (light/dark mode) when appropriate.
- Take screenshots or pause tests for debugging.
<!-- Pest Browser Test Example -->
```php
it('may reset the password', function () {
Notification::fake();
$this->actingAs(User::factory()->create());
$page = visit('/sign-in');
$page->assertSee('Sign In')
->assertNoJavaScriptErrors()
->click('Forgot Password?')
->fill('email', 'nuno@laravel.com')
->click('Send Reset Link')
->assertSee('We have emailed your password reset link!');
Notification::assertSent(ResetPassword::class);
});
```
### Smoke Testing
Quickly validate multiple pages have no JavaScript errors:
<!-- Pest Smoke Testing Example -->
```php
$pages = visit(['/', '/about', '/contact']);
$pages->assertNoJavaScriptErrors()->assertNoConsoleLogs();
```
### Visual Regression Testing
Capture and compare screenshots to detect visual changes.
### Test Sharding
Split tests across parallel processes for faster CI runs.
### Architecture Testing
Pest 4 includes architecture testing (from Pest 3):
<!-- Architecture Test Example -->
```php
arch('controllers')
->expect('App\Http\Controllers')
->toExtendNothing()
->toHaveSuffix('Controller');
```
## Common Pitfalls
- Not importing `use function Pest\Laravel\mock;` before using mock
- Using `assertStatus(200)` instead of `assertSuccessful()`
- Forgetting datasets for repetitive validation tests
- Deleting tests without approval
- Forgetting `assertNoJavaScriptErrors()` in browser tests

20
.cursor/mcp.json Normal file
View File

@@ -0,0 +1,20 @@
{
"mcpServers": {
"laravel-boost": {
"command": "php",
"args": [
"artisan",
"boost:mcp"
]
},
"herd": {
"command": "php",
"args": [
"/Applications/Herd.app/Contents/Resources/herd-mcp.phar"
],
"env": {
"SITE_PATH": "/Users/bulutkuru/Herd/bogazici-api"
}
}
}
}

View File

@@ -0,0 +1,167 @@
---
name: pest-testing
description: "Tests applications using the Pest 4 PHP framework. Activates when writing tests, creating unit or feature tests, adding assertions, testing Livewire components, browser testing, debugging test failures, working with datasets or mocking; or when the user mentions test, spec, TDD, expects, assertion, coverage, or needs to verify functionality works."
license: MIT
metadata:
author: laravel
---
# Pest Testing 4
## When to Apply
Activate this skill when:
- Creating new tests (unit, feature, or browser)
- Modifying existing tests
- Debugging test failures
- Working with browser testing or smoke testing
- Writing architecture tests or visual regression tests
## Documentation
Use `search-docs` for detailed Pest 4 patterns and documentation.
## Basic Usage
### Creating Tests
All tests must be written using Pest. Use `php artisan make:test --pest {name}`.
### Test Organization
- Unit/Feature tests: `tests/Feature` and `tests/Unit` directories.
- Browser tests: `tests/Browser/` directory.
- Do NOT remove tests without approval - these are core application code.
### Basic Test Structure
<!-- Basic Pest Test Example -->
```php
it('is true', function () {
expect(true)->toBeTrue();
});
```
### Running Tests
- Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`.
- Run all tests: `php artisan test --compact`.
- Run file: `php artisan test --compact tests/Feature/ExampleTest.php`.
## Assertions
Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`:
<!-- Pest Response Assertion -->
```php
it('returns all', function () {
$this->postJson('/api/docs', [])->assertSuccessful();
});
```
| Use | Instead of |
|-----|------------|
| `assertSuccessful()` | `assertStatus(200)` |
| `assertNotFound()` | `assertStatus(404)` |
| `assertForbidden()` | `assertStatus(403)` |
## Mocking
Import mock function before use: `use function Pest\Laravel\mock;`
## Datasets
Use datasets for repetitive tests (validation rules, etc.):
<!-- Pest Dataset Example -->
```php
it('has emails', function (string $email) {
expect($email)->not->toBeEmpty();
})->with([
'james' => 'james@laravel.com',
'taylor' => 'taylor@laravel.com',
]);
```
## Pest 4 Features
| Feature | Purpose |
|---------|---------|
| Browser Testing | Full integration tests in real browsers |
| Smoke Testing | Validate multiple pages quickly |
| Visual Regression | Compare screenshots for visual changes |
| Test Sharding | Parallel CI runs |
| Architecture Testing | Enforce code conventions |
### Browser Test Example
Browser tests run in real browsers for full integration testing:
- Browser tests live in `tests/Browser/`.
- Use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories.
- Use `RefreshDatabase` for clean state per test.
- Interact with page: click, type, scroll, select, submit, drag-and-drop, touch gestures.
- Test on multiple browsers (Chrome, Firefox, Safari) if requested.
- Test on different devices/viewports (iPhone 14 Pro, tablets) if requested.
- Switch color schemes (light/dark mode) when appropriate.
- Take screenshots or pause tests for debugging.
<!-- Pest Browser Test Example -->
```php
it('may reset the password', function () {
Notification::fake();
$this->actingAs(User::factory()->create());
$page = visit('/sign-in');
$page->assertSee('Sign In')
->assertNoJavaScriptErrors()
->click('Forgot Password?')
->fill('email', 'nuno@laravel.com')
->click('Send Reset Link')
->assertSee('We have emailed your password reset link!');
Notification::assertSent(ResetPassword::class);
});
```
### Smoke Testing
Quickly validate multiple pages have no JavaScript errors:
<!-- Pest Smoke Testing Example -->
```php
$pages = visit(['/', '/about', '/contact']);
$pages->assertNoJavaScriptErrors()->assertNoConsoleLogs();
```
### Visual Regression Testing
Capture and compare screenshots to detect visual changes.
### Test Sharding
Split tests across parallel processes for faster CI runs.
### Architecture Testing
Pest 4 includes architecture testing (from Pest 3):
<!-- Architecture Test Example -->
```php
arch('controllers')
->expect('App\Http\Controllers')
->toExtendNothing()
->toHaveSuffix('Controller');
```
## Common Pitfalls
- Not importing `use function Pest\Laravel\mock;` before using mock
- Using `assertStatus(200)` instead of `assertSuccessful()`
- Forgetting datasets for repetitive validation tests
- Deleting tests without approval
- Forgetting `assertNoJavaScriptErrors()` in browser tests

57
.drone.yml Normal file
View File

@@ -0,0 +1,57 @@
kind: pipeline
type: docker
name: default
trigger:
branch:
- develop
- test
- main
steps:
- name: laravel-check
image: php:8.4-cli
when:
branch:
- develop
commands:
- apt-get update && apt-get install -y git unzip curl libzip-dev default-mysql-client
- docker-php-ext-install pdo_mysql zip
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
- cp .env.example .env || true
- composer install --no-interaction --prefer-dist --optimize-autoloader
- php artisan key:generate --force || true
- php artisan config:clear || true
- php artisan test || true
- name: deploy-test
image: appleboy/drone-ssh
when:
branch:
- test
settings:
host:
from_secret: server_host
username:
from_secret: server_user
key:
from_secret: server_ssh_key
script:
- cd /opt/projects/bogazici/corporate-api/test/api
- bash scripts/deploy-test.sh
- name: deploy-prod
image: appleboy/drone-ssh
when:
branch:
- main
settings:
host:
from_secret: server_host
username:
from_secret: server_user
key:
from_secret: server_ssh_key
script:
- cd /opt/projects/bogazici/corporate-api/prod/api
- bash scripts/deploy-prod.sh

18
.editorconfig Normal file
View File

@@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[compose.yaml]
indent_size = 4

65
.env.example Normal file
View File

@@ -0,0 +1,65 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database
# PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=bogazici_api
DB_USERNAME=root
DB_PASSWORD=
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database
# CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"

11
.gitattributes vendored Normal file
View File

@@ -0,0 +1,11 @@
* text=auto eol=lf
*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
/.github export-ignore
CHANGELOG.md export-ignore
.styleci.yml export-ignore

33
.gitignore vendored Normal file
View File

@@ -0,0 +1,33 @@
*.log
.DS_Store
.env
.env.backup
.env.production
.phpactor.json
.phpunit.result.cache
/.fleet
/.idea
/.nova
/.phpunit.cache
/.vscode
/.zed
/auth.json
/node_modules
/public/build
/public/hot
/public/storage
/public/uploads/images/*
/public/uploads/videos/*
/public/uploads/hero-slides/*
/public/uploads/settings/*
/public/uploads/pages/*
/public/uploads/courses/*
/public/uploads/announcements/*
/public/uploads/categories/*
!/public/uploads/*/.gitkeep
/storage/*.key
/storage/pail
/vendor
Homestead.json
Homestead.yaml
Thumbs.db

238
.junie/guidelines.md Normal file
View File

@@ -0,0 +1,238 @@
<laravel-boost-guidelines>
=== foundation rules ===
# Laravel Boost Guidelines
The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to ensure the best experience when building Laravel applications.
## Foundational Context
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
- php - 8.4.18
- laravel/framework (LARAVEL) - v12
- laravel/prompts (PROMPTS) - v0
- laravel/sanctum (SANCTUM) - v4
- laravel/boost (BOOST) - v2
- laravel/mcp (MCP) - v0
- laravel/pail (PAIL) - v1
- laravel/pint (PINT) - v1
- laravel/sail (SAIL) - v1
- pestphp/pest (PEST) - v4
- phpunit/phpunit (PHPUNIT) - v12
## Skills Activation
This project has domain-specific skills available. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck.
- `pest-testing` — Tests applications using the Pest 4 PHP framework. Activates when writing tests, creating unit or feature tests, adding assertions, testing Livewire components, browser testing, debugging test failures, working with datasets or mocking; or when the user mentions test, spec, TDD, expects, assertion, coverage, or needs to verify functionality works.
## Conventions
- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming.
- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`.
- Check for existing components to reuse before writing a new one.
## Verification Scripts
- Do not create verification scripts or tinker when tests cover that functionality and prove they work. Unit and feature tests are more important.
## Application Structure & Architecture
- Stick to existing directory structure; don't create new base folders without approval.
- Do not change the application's dependencies without approval.
## Frontend Bundling
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them.
## Documentation Files
- You must only create documentation files if explicitly requested by the user.
## Replies
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
=== boost rules ===
# Laravel Boost
- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them.
## Artisan
- Use the `list-artisan-commands` tool when you need to call an Artisan command to double-check the available parameters.
## URLs
- Whenever you share a project URL with the user, you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain/IP, and port.
## Tinker / Debugging
- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly.
- Use the `database-query` tool when you only need to read from the database.
- Use the `database-schema` tool to inspect table structure before writing migrations or models.
## Reading Browser Logs With the `browser-logs` Tool
- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost.
- Only recent browser logs will be useful - ignore old logs.
## Searching Documentation (Critically Important)
- Boost comes with a powerful `search-docs` tool you should use before trying other approaches when working with Laravel or Laravel ecosystem packages. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages.
- Search the documentation before making code changes to ensure we are taking the correct approach.
- Use multiple, broad, simple, topic-based queries at once. For example: `['rate limiting', 'routing rate limiting', 'routing']`. The most relevant results will be returned first.
- Do not add package names to queries; package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`.
### Available Search Syntax
1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth'.
2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit".
3. Quoted Phrases (Exact Position) - query="infinite scroll" - words must be adjacent and in that order.
4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit".
5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms.
=== php rules ===
# PHP
- Always use curly braces for control structures, even for single-line bodies.
## Constructors
- Use PHP 8 constructor property promotion in `__construct()`.
- `public function __construct(public GitHub $github) { }`
- Do not allow empty `__construct()` methods with zero parameters unless the constructor is private.
## Type Declarations
- Always use explicit return type declarations for methods and functions.
- Use appropriate PHP type hints for method parameters.
<!-- Explicit Return Types and Method Params -->
```php
protected function isAccessible(User $user, ?string $path = null): bool
{
...
}
```
## Enums
- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`.
## Comments
- Prefer PHPDoc blocks over inline comments. Never use comments within the code itself unless the logic is exceptionally complex.
## PHPDoc Blocks
- Add useful array shape type definitions when appropriate.
=== herd rules ===
# Laravel Herd
- The application is served by Laravel Herd and will be available at: `https?://[kebab-case-project-dir].test`. Use the `get-absolute-url` tool to generate valid URLs for the user.
- You must not run any commands to make the site available via HTTP(S). It is always available through Laravel Herd.
=== laravel/core rules ===
# Do Things the Laravel Way
- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool.
- If you're creating a generic PHP class, use `php artisan make:class`.
- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior.
## Database
- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins.
- Use Eloquent models and relationships before suggesting raw database queries.
- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them.
- Generate code that prevents N+1 query problems by using eager loading.
- Use Laravel's query builder for very complex database operations.
### Model Creation
- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `php artisan make:model`.
### APIs & Eloquent Resources
- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.
## Controllers & Validation
- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages.
- Check sibling Form Requests to see if the application uses array or string based validation rules.
## Authentication & Authorization
- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.).
## URL Generation
- When generating links to other pages, prefer named routes and the `route()` function.
## Queues
- Use queued jobs for time-consuming operations with the `ShouldQueue` interface.
## Configuration
- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`.
## Testing
- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`.
- When creating tests, make use of `php artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests.
## Vite Error
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`.
=== laravel/v12 rules ===
# Laravel 12
- CRITICAL: ALWAYS use `search-docs` tool for version-specific Laravel documentation and updated code examples.
- Since Laravel 11, Laravel has a new streamlined file structure which this project uses.
## Laravel 12 Structure
- In Laravel 12, middleware are no longer registered in `app/Http/Kernel.php`.
- Middleware are configured declaratively in `bootstrap/app.php` using `Application::configure()->withMiddleware()`.
- `bootstrap/app.php` is the file to register middleware, exceptions, and routing files.
- `bootstrap/providers.php` contains application specific service providers.
- The `app\Console\Kernel.php` file no longer exists; use `bootstrap/app.php` or `routes/console.php` for console configuration.
- Console commands in `app/Console/Commands/` are automatically available and do not require manual registration.
## Database
- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost.
- Laravel 12 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`.
### Models
- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models.
=== pint/core rules ===
# Laravel Pint Code Formatter
- If you have modified any PHP files, you must run `vendor/bin/pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style.
- Do not run `vendor/bin/pint --test --format agent`, simply run `vendor/bin/pint --format agent` to fix any formatting issues.
=== pest/core rules ===
## Pest
- This project uses Pest for testing. Create tests: `php artisan make:test --pest {name}`.
- Run tests: `php artisan test --compact` or filter: `php artisan test --compact --filter=testName`.
- Do NOT delete tests without approval.
- CRITICAL: ALWAYS use `search-docs` tool for version-specific Pest documentation and updated code examples.
- IMPORTANT: Activate `pest-testing` every time you're working with a Pest or testing-related task.
</laravel-boost-guidelines>

20
.junie/mcp/mcp.json Normal file
View File

@@ -0,0 +1,20 @@
{
"mcpServers": {
"laravel-boost": {
"command": "/Users/bulutkuru/Library/Application Support/Herd/bin/php84",
"args": [
"/Users/bulutkuru/Herd/bogazici-api/artisan",
"boost:mcp"
]
},
"herd": {
"command": "/Users/bulutkuru/Library/Application Support/Herd/bin/php84",
"args": [
"/Applications/Herd.app/Contents/Resources/herd-mcp.phar"
],
"env": {
"SITE_PATH": "/Users/bulutkuru/Herd/bogazici-api"
}
}
}
}

View File

@@ -0,0 +1,167 @@
---
name: pest-testing
description: "Tests applications using the Pest 4 PHP framework. Activates when writing tests, creating unit or feature tests, adding assertions, testing Livewire components, browser testing, debugging test failures, working with datasets or mocking; or when the user mentions test, spec, TDD, expects, assertion, coverage, or needs to verify functionality works."
license: MIT
metadata:
author: laravel
---
# Pest Testing 4
## When to Apply
Activate this skill when:
- Creating new tests (unit, feature, or browser)
- Modifying existing tests
- Debugging test failures
- Working with browser testing or smoke testing
- Writing architecture tests or visual regression tests
## Documentation
Use `search-docs` for detailed Pest 4 patterns and documentation.
## Basic Usage
### Creating Tests
All tests must be written using Pest. Use `php artisan make:test --pest {name}`.
### Test Organization
- Unit/Feature tests: `tests/Feature` and `tests/Unit` directories.
- Browser tests: `tests/Browser/` directory.
- Do NOT remove tests without approval - these are core application code.
### Basic Test Structure
<!-- Basic Pest Test Example -->
```php
it('is true', function () {
expect(true)->toBeTrue();
});
```
### Running Tests
- Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`.
- Run all tests: `php artisan test --compact`.
- Run file: `php artisan test --compact tests/Feature/ExampleTest.php`.
## Assertions
Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`:
<!-- Pest Response Assertion -->
```php
it('returns all', function () {
$this->postJson('/api/docs', [])->assertSuccessful();
});
```
| Use | Instead of |
|-----|------------|
| `assertSuccessful()` | `assertStatus(200)` |
| `assertNotFound()` | `assertStatus(404)` |
| `assertForbidden()` | `assertStatus(403)` |
## Mocking
Import mock function before use: `use function Pest\Laravel\mock;`
## Datasets
Use datasets for repetitive tests (validation rules, etc.):
<!-- Pest Dataset Example -->
```php
it('has emails', function (string $email) {
expect($email)->not->toBeEmpty();
})->with([
'james' => 'james@laravel.com',
'taylor' => 'taylor@laravel.com',
]);
```
## Pest 4 Features
| Feature | Purpose |
|---------|---------|
| Browser Testing | Full integration tests in real browsers |
| Smoke Testing | Validate multiple pages quickly |
| Visual Regression | Compare screenshots for visual changes |
| Test Sharding | Parallel CI runs |
| Architecture Testing | Enforce code conventions |
### Browser Test Example
Browser tests run in real browsers for full integration testing:
- Browser tests live in `tests/Browser/`.
- Use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories.
- Use `RefreshDatabase` for clean state per test.
- Interact with page: click, type, scroll, select, submit, drag-and-drop, touch gestures.
- Test on multiple browsers (Chrome, Firefox, Safari) if requested.
- Test on different devices/viewports (iPhone 14 Pro, tablets) if requested.
- Switch color schemes (light/dark mode) when appropriate.
- Take screenshots or pause tests for debugging.
<!-- Pest Browser Test Example -->
```php
it('may reset the password', function () {
Notification::fake();
$this->actingAs(User::factory()->create());
$page = visit('/sign-in');
$page->assertSee('Sign In')
->assertNoJavaScriptErrors()
->click('Forgot Password?')
->fill('email', 'nuno@laravel.com')
->click('Send Reset Link')
->assertSee('We have emailed your password reset link!');
Notification::assertSent(ResetPassword::class);
});
```
### Smoke Testing
Quickly validate multiple pages have no JavaScript errors:
<!-- Pest Smoke Testing Example -->
```php
$pages = visit(['/', '/about', '/contact']);
$pages->assertNoJavaScriptErrors()->assertNoConsoleLogs();
```
### Visual Regression Testing
Capture and compare screenshots to detect visual changes.
### Test Sharding
Split tests across parallel processes for faster CI runs.
### Architecture Testing
Pest 4 includes architecture testing (from Pest 3):
<!-- Architecture Test Example -->
```php
arch('controllers')
->expect('App\Http\Controllers')
->toExtendNothing()
->toHaveSuffix('Controller');
```
## Common Pitfalls
- Not importing `use function Pest\Laravel\mock;` before using mock
- Using `assertStatus(200)` instead of `assertSuccessful()`
- Forgetting datasets for repetitive validation tests
- Deleting tests without approval
- Forgetting `assertNoJavaScriptErrors()` in browser tests

20
.mcp.json Normal file
View File

@@ -0,0 +1,20 @@
{
"mcpServers": {
"laravel-boost": {
"command": "php",
"args": [
"artisan",
"boost:mcp"
]
},
"herd": {
"command": "php",
"args": [
"/Applications/Herd.app/Contents/Resources/herd-mcp.phar"
],
"env": {
"SITE_PATH": "/Users/bulutkuru/Herd/bogazici-api"
}
}
}
}

242
AGENTS.md Normal file
View File

@@ -0,0 +1,242 @@
<laravel-boost-guidelines>
=== foundation rules ===
# Laravel Boost Guidelines
The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to ensure the best experience when building Laravel applications.
## Foundational Context
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
- php - 8.4.19
- laravel/framework (LARAVEL) - v12
- laravel/prompts (PROMPTS) - v0
- laravel/sanctum (SANCTUM) - v4
- laravel/boost (BOOST) - v2
- laravel/mcp (MCP) - v0
- laravel/pail (PAIL) - v1
- laravel/pint (PINT) - v1
- laravel/sail (SAIL) - v1
- pestphp/pest (PEST) - v4
- phpunit/phpunit (PHPUNIT) - v12
## Skills Activation
This project has domain-specific skills available. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck.
- `pest-testing` — Tests applications using the Pest 4 PHP framework. Activates when writing tests, creating unit or feature tests, adding assertions, testing Livewire components, browser testing, debugging test failures, working with datasets or mocking; or when the user mentions test, spec, TDD, expects, assertion, coverage, or needs to verify functionality works.
## Conventions
- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming.
- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`.
- Check for existing components to reuse before writing a new one.
## Verification Scripts
- Do not create verification scripts or tinker when tests cover that functionality and prove they work. Unit and feature tests are more important.
## Application Structure & Architecture
- Stick to existing directory structure; don't create new base folders without approval.
- Do not change the application's dependencies without approval.
## Frontend Bundling
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them.
## Documentation Files
- You must only create documentation files if explicitly requested by the user.
## Replies
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
=== boost rules ===
# Laravel Boost
- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them.
## Artisan Commands
- Run Artisan commands directly via the command line (e.g., `php artisan route:list`, `php artisan tinker --execute "..."`).
- Use `php artisan list` to discover available commands and `php artisan [command] --help` to check parameters.
## URLs
- Whenever you share a project URL with the user, you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain/IP, and port.
## Debugging
- Use the `database-query` tool when you only need to read from the database.
- Use the `database-schema` tool to inspect table structure before writing migrations or models.
- To execute PHP code for debugging, run `php artisan tinker --execute "your code here"` directly.
- To read configuration values, read the config files directly or run `php artisan config:show [key]`.
- To inspect routes, run `php artisan route:list` directly.
- To check environment variables, read the `.env` file directly.
## Reading Browser Logs With the `browser-logs` Tool
- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost.
- Only recent browser logs will be useful - ignore old logs.
## Searching Documentation (Critically Important)
- Boost comes with a powerful `search-docs` tool you should use before trying other approaches when working with Laravel or Laravel ecosystem packages. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages.
- Search the documentation before making code changes to ensure we are taking the correct approach.
- Use multiple, broad, simple, topic-based queries at once. For example: `['rate limiting', 'routing rate limiting', 'routing']`. The most relevant results will be returned first.
- Do not add package names to queries; package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`.
### Available Search Syntax
1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth'.
2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit".
3. Quoted Phrases (Exact Position) - query="infinite scroll" - words must be adjacent and in that order.
4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit".
5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms.
=== php rules ===
# PHP
- Always use curly braces for control structures, even for single-line bodies.
## Constructors
- Use PHP 8 constructor property promotion in `__construct()`.
- `public function __construct(public GitHub $github) { }`
- Do not allow empty `__construct()` methods with zero parameters unless the constructor is private.
## Type Declarations
- Always use explicit return type declarations for methods and functions.
- Use appropriate PHP type hints for method parameters.
<!-- Explicit Return Types and Method Params -->
```php
protected function isAccessible(User $user, ?string $path = null): bool
{
...
}
```
## Enums
- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`.
## Comments
- Prefer PHPDoc blocks over inline comments. Never use comments within the code itself unless the logic is exceptionally complex.
## PHPDoc Blocks
- Add useful array shape type definitions when appropriate.
=== herd rules ===
# Laravel Herd
- The application is served by Laravel Herd and will be available at: `https?://[kebab-case-project-dir].test`. Use the `get-absolute-url` tool to generate valid URLs for the user.
- You must not run any commands to make the site available via HTTP(S). It is always available through Laravel Herd.
=== laravel/core rules ===
# Do Things the Laravel Way
- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using `php artisan list` and check their parameters with `php artisan [command] --help`.
- If you're creating a generic PHP class, use `php artisan make:class`.
- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior.
## Database
- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins.
- Use Eloquent models and relationships before suggesting raw database queries.
- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them.
- Generate code that prevents N+1 query problems by using eager loading.
- Use Laravel's query builder for very complex database operations.
### Model Creation
- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `php artisan make:model --help` to check the available options.
### APIs & Eloquent Resources
- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.
## Controllers & Validation
- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages.
- Check sibling Form Requests to see if the application uses array or string based validation rules.
## Authentication & Authorization
- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.).
## URL Generation
- When generating links to other pages, prefer named routes and the `route()` function.
## Queues
- Use queued jobs for time-consuming operations with the `ShouldQueue` interface.
## Configuration
- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`.
## Testing
- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`.
- When creating tests, make use of `php artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests.
## Vite Error
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`.
=== laravel/v12 rules ===
# Laravel 12
- CRITICAL: ALWAYS use `search-docs` tool for version-specific Laravel documentation and updated code examples.
- Since Laravel 11, Laravel has a new streamlined file structure which this project uses.
## Laravel 12 Structure
- In Laravel 12, middleware are no longer registered in `app/Http/Kernel.php`.
- Middleware are configured declaratively in `bootstrap/app.php` using `Application::configure()->withMiddleware()`.
- `bootstrap/app.php` is the file to register middleware, exceptions, and routing files.
- `bootstrap/providers.php` contains application specific service providers.
- The `app\Console\Kernel.php` file no longer exists; use `bootstrap/app.php` or `routes/console.php` for console configuration.
- Console commands in `app/Console/Commands/` are automatically available and do not require manual registration.
## Database
- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost.
- Laravel 12 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`.
### Models
- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models.
=== pint/core rules ===
# Laravel Pint Code Formatter
- If you have modified any PHP files, you must run `vendor/bin/pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style.
- Do not run `vendor/bin/pint --test --format agent`, simply run `vendor/bin/pint --format agent` to fix any formatting issues.
=== pest/core rules ===
## Pest
- This project uses Pest for testing. Create tests: `php artisan make:test --pest {name}`.
- Run tests: `php artisan test --compact` or filter: `php artisan test --compact --filter=testName`.
- Do NOT delete tests without approval.
- CRITICAL: ALWAYS use `search-docs` tool for version-specific Pest documentation and updated code examples.
- IMPORTANT: Activate `pest-testing` every time you're working with a Pest or testing-related task.
</laravel-boost-guidelines>

242
CLAUDE.md Normal file
View File

@@ -0,0 +1,242 @@
<laravel-boost-guidelines>
=== foundation rules ===
# Laravel Boost Guidelines
The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to ensure the best experience when building Laravel applications.
## Foundational Context
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
- php - 8.4.19
- laravel/framework (LARAVEL) - v12
- laravel/prompts (PROMPTS) - v0
- laravel/sanctum (SANCTUM) - v4
- laravel/boost (BOOST) - v2
- laravel/mcp (MCP) - v0
- laravel/pail (PAIL) - v1
- laravel/pint (PINT) - v1
- laravel/sail (SAIL) - v1
- pestphp/pest (PEST) - v4
- phpunit/phpunit (PHPUNIT) - v12
## Skills Activation
This project has domain-specific skills available. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck.
- `pest-testing` — Tests applications using the Pest 4 PHP framework. Activates when writing tests, creating unit or feature tests, adding assertions, testing Livewire components, browser testing, debugging test failures, working with datasets or mocking; or when the user mentions test, spec, TDD, expects, assertion, coverage, or needs to verify functionality works.
## Conventions
- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming.
- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`.
- Check for existing components to reuse before writing a new one.
## Verification Scripts
- Do not create verification scripts or tinker when tests cover that functionality and prove they work. Unit and feature tests are more important.
## Application Structure & Architecture
- Stick to existing directory structure; don't create new base folders without approval.
- Do not change the application's dependencies without approval.
## Frontend Bundling
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them.
## Documentation Files
- You must only create documentation files if explicitly requested by the user.
## Replies
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
=== boost rules ===
# Laravel Boost
- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them.
## Artisan Commands
- Run Artisan commands directly via the command line (e.g., `php artisan route:list`, `php artisan tinker --execute "..."`).
- Use `php artisan list` to discover available commands and `php artisan [command] --help` to check parameters.
## URLs
- Whenever you share a project URL with the user, you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain/IP, and port.
## Debugging
- Use the `database-query` tool when you only need to read from the database.
- Use the `database-schema` tool to inspect table structure before writing migrations or models.
- To execute PHP code for debugging, run `php artisan tinker --execute "your code here"` directly.
- To read configuration values, read the config files directly or run `php artisan config:show [key]`.
- To inspect routes, run `php artisan route:list` directly.
- To check environment variables, read the `.env` file directly.
## Reading Browser Logs With the `browser-logs` Tool
- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost.
- Only recent browser logs will be useful - ignore old logs.
## Searching Documentation (Critically Important)
- Boost comes with a powerful `search-docs` tool you should use before trying other approaches when working with Laravel or Laravel ecosystem packages. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages.
- Search the documentation before making code changes to ensure we are taking the correct approach.
- Use multiple, broad, simple, topic-based queries at once. For example: `['rate limiting', 'routing rate limiting', 'routing']`. The most relevant results will be returned first.
- Do not add package names to queries; package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`.
### Available Search Syntax
1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth'.
2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit".
3. Quoted Phrases (Exact Position) - query="infinite scroll" - words must be adjacent and in that order.
4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit".
5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms.
=== php rules ===
# PHP
- Always use curly braces for control structures, even for single-line bodies.
## Constructors
- Use PHP 8 constructor property promotion in `__construct()`.
- `public function __construct(public GitHub $github) { }`
- Do not allow empty `__construct()` methods with zero parameters unless the constructor is private.
## Type Declarations
- Always use explicit return type declarations for methods and functions.
- Use appropriate PHP type hints for method parameters.
<!-- Explicit Return Types and Method Params -->
```php
protected function isAccessible(User $user, ?string $path = null): bool
{
...
}
```
## Enums
- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`.
## Comments
- Prefer PHPDoc blocks over inline comments. Never use comments within the code itself unless the logic is exceptionally complex.
## PHPDoc Blocks
- Add useful array shape type definitions when appropriate.
=== herd rules ===
# Laravel Herd
- The application is served by Laravel Herd and will be available at: `https?://[kebab-case-project-dir].test`. Use the `get-absolute-url` tool to generate valid URLs for the user.
- You must not run any commands to make the site available via HTTP(S). It is always available through Laravel Herd.
=== laravel/core rules ===
# Do Things the Laravel Way
- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using `php artisan list` and check their parameters with `php artisan [command] --help`.
- If you're creating a generic PHP class, use `php artisan make:class`.
- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior.
## Database
- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins.
- Use Eloquent models and relationships before suggesting raw database queries.
- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them.
- Generate code that prevents N+1 query problems by using eager loading.
- Use Laravel's query builder for very complex database operations.
### Model Creation
- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `php artisan make:model --help` to check the available options.
### APIs & Eloquent Resources
- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.
## Controllers & Validation
- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages.
- Check sibling Form Requests to see if the application uses array or string based validation rules.
## Authentication & Authorization
- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.).
## URL Generation
- When generating links to other pages, prefer named routes and the `route()` function.
## Queues
- Use queued jobs for time-consuming operations with the `ShouldQueue` interface.
## Configuration
- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`.
## Testing
- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`.
- When creating tests, make use of `php artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests.
## Vite Error
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`.
=== laravel/v12 rules ===
# Laravel 12
- CRITICAL: ALWAYS use `search-docs` tool for version-specific Laravel documentation and updated code examples.
- Since Laravel 11, Laravel has a new streamlined file structure which this project uses.
## Laravel 12 Structure
- In Laravel 12, middleware are no longer registered in `app/Http/Kernel.php`.
- Middleware are configured declaratively in `bootstrap/app.php` using `Application::configure()->withMiddleware()`.
- `bootstrap/app.php` is the file to register middleware, exceptions, and routing files.
- `bootstrap/providers.php` contains application specific service providers.
- The `app\Console\Kernel.php` file no longer exists; use `bootstrap/app.php` or `routes/console.php` for console configuration.
- Console commands in `app/Console/Commands/` are automatically available and do not require manual registration.
## Database
- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost.
- Laravel 12 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`.
### Models
- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models.
=== pint/core rules ===
# Laravel Pint Code Formatter
- If you have modified any PHP files, you must run `vendor/bin/pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style.
- Do not run `vendor/bin/pint --test --format agent`, simply run `vendor/bin/pint --format agent` to fix any formatting issues.
=== pest/core rules ===
## Pest
- This project uses Pest for testing. Create tests: `php artisan make:test --pest {name}`.
- Run tests: `php artisan test --compact` or filter: `php artisan test --compact --filter=testName`.
- Do NOT delete tests without approval.
- CRITICAL: ALWAYS use `search-docs` tool for version-specific Pest documentation and updated code examples.
- IMPORTANT: Activate `pest-testing` every time you're working with a Pest or testing-related task.
</laravel-boost-guidelines>

54
Dockerfile Normal file
View File

@@ -0,0 +1,54 @@
FROM php:8.4-apache
WORKDIR /var/www/html
RUN apt-get update && apt-get install -y \
build-essential \
libpng-dev \
libjpeg62-turbo-dev \
libfreetype6-dev \
locales \
zip \
jpegoptim \
optipng \
pngquant \
gifsicle \
vim \
nano \
unzip \
libzip-dev \
libicu-dev \
git \
curl \
default-mysql-client \
libmagickwand-dev \
--no-install-recommends \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
RUN printf "\n" | pecl install imagick \
&& docker-php-ext-enable imagick
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) gd pdo_mysql exif zip bcmath intl
RUN a2enmod rewrite
# PHP upload limits
RUN echo "upload_max_filesize=110M" > /usr/local/etc/php/conf.d/uploads.ini \
&& echo "post_max_size=120M" >> /usr/local/etc/php/conf.d/uploads.ini \
&& echo "memory_limit=256M" >> /usr/local/etc/php/conf.d/uploads.ini
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
COPY . /var/www/html
COPY docker/apache/000-default.conf /etc/apache2/sites-available/000-default.conf
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh \
&& mkdir -p storage bootstrap/cache \
&& chown -R www-data:www-data /var/www/html \
&& chmod -R 775 storage bootstrap/cache
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]

242
GEMINI.md Normal file
View File

@@ -0,0 +1,242 @@
<laravel-boost-guidelines>
=== foundation rules ===
# Laravel Boost Guidelines
The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to ensure the best experience when building Laravel applications.
## Foundational Context
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
- php - 8.4.19
- laravel/framework (LARAVEL) - v12
- laravel/prompts (PROMPTS) - v0
- laravel/sanctum (SANCTUM) - v4
- laravel/boost (BOOST) - v2
- laravel/mcp (MCP) - v0
- laravel/pail (PAIL) - v1
- laravel/pint (PINT) - v1
- laravel/sail (SAIL) - v1
- pestphp/pest (PEST) - v4
- phpunit/phpunit (PHPUNIT) - v12
## Skills Activation
This project has domain-specific skills available. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck.
- `pest-testing` — Tests applications using the Pest 4 PHP framework. Activates when writing tests, creating unit or feature tests, adding assertions, testing Livewire components, browser testing, debugging test failures, working with datasets or mocking; or when the user mentions test, spec, TDD, expects, assertion, coverage, or needs to verify functionality works.
## Conventions
- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming.
- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`.
- Check for existing components to reuse before writing a new one.
## Verification Scripts
- Do not create verification scripts or tinker when tests cover that functionality and prove they work. Unit and feature tests are more important.
## Application Structure & Architecture
- Stick to existing directory structure; don't create new base folders without approval.
- Do not change the application's dependencies without approval.
## Frontend Bundling
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them.
## Documentation Files
- You must only create documentation files if explicitly requested by the user.
## Replies
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
=== boost rules ===
# Laravel Boost
- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them.
## Artisan Commands
- Run Artisan commands directly via the command line (e.g., `php artisan route:list`, `php artisan tinker --execute "..."`).
- Use `php artisan list` to discover available commands and `php artisan [command] --help` to check parameters.
## URLs
- Whenever you share a project URL with the user, you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain/IP, and port.
## Debugging
- Use the `database-query` tool when you only need to read from the database.
- Use the `database-schema` tool to inspect table structure before writing migrations or models.
- To execute PHP code for debugging, run `php artisan tinker --execute "your code here"` directly.
- To read configuration values, read the config files directly or run `php artisan config:show [key]`.
- To inspect routes, run `php artisan route:list` directly.
- To check environment variables, read the `.env` file directly.
## Reading Browser Logs With the `browser-logs` Tool
- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost.
- Only recent browser logs will be useful - ignore old logs.
## Searching Documentation (Critically Important)
- Boost comes with a powerful `search-docs` tool you should use before trying other approaches when working with Laravel or Laravel ecosystem packages. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages.
- Search the documentation before making code changes to ensure we are taking the correct approach.
- Use multiple, broad, simple, topic-based queries at once. For example: `['rate limiting', 'routing rate limiting', 'routing']`. The most relevant results will be returned first.
- Do not add package names to queries; package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`.
### Available Search Syntax
1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth'.
2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit".
3. Quoted Phrases (Exact Position) - query="infinite scroll" - words must be adjacent and in that order.
4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit".
5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms.
=== php rules ===
# PHP
- Always use curly braces for control structures, even for single-line bodies.
## Constructors
- Use PHP 8 constructor property promotion in `__construct()`.
- `public function __construct(public GitHub $github) { }`
- Do not allow empty `__construct()` methods with zero parameters unless the constructor is private.
## Type Declarations
- Always use explicit return type declarations for methods and functions.
- Use appropriate PHP type hints for method parameters.
<!-- Explicit Return Types and Method Params -->
```php
protected function isAccessible(User $user, ?string $path = null): bool
{
...
}
```
## Enums
- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`.
## Comments
- Prefer PHPDoc blocks over inline comments. Never use comments within the code itself unless the logic is exceptionally complex.
## PHPDoc Blocks
- Add useful array shape type definitions when appropriate.
=== herd rules ===
# Laravel Herd
- The application is served by Laravel Herd and will be available at: `https?://[kebab-case-project-dir].test`. Use the `get-absolute-url` tool to generate valid URLs for the user.
- You must not run any commands to make the site available via HTTP(S). It is always available through Laravel Herd.
=== laravel/core rules ===
# Do Things the Laravel Way
- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using `php artisan list` and check their parameters with `php artisan [command] --help`.
- If you're creating a generic PHP class, use `php artisan make:class`.
- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior.
## Database
- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins.
- Use Eloquent models and relationships before suggesting raw database queries.
- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them.
- Generate code that prevents N+1 query problems by using eager loading.
- Use Laravel's query builder for very complex database operations.
### Model Creation
- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `php artisan make:model --help` to check the available options.
### APIs & Eloquent Resources
- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.
## Controllers & Validation
- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages.
- Check sibling Form Requests to see if the application uses array or string based validation rules.
## Authentication & Authorization
- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.).
## URL Generation
- When generating links to other pages, prefer named routes and the `route()` function.
## Queues
- Use queued jobs for time-consuming operations with the `ShouldQueue` interface.
## Configuration
- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`.
## Testing
- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`.
- When creating tests, make use of `php artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests.
## Vite Error
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`.
=== laravel/v12 rules ===
# Laravel 12
- CRITICAL: ALWAYS use `search-docs` tool for version-specific Laravel documentation and updated code examples.
- Since Laravel 11, Laravel has a new streamlined file structure which this project uses.
## Laravel 12 Structure
- In Laravel 12, middleware are no longer registered in `app/Http/Kernel.php`.
- Middleware are configured declaratively in `bootstrap/app.php` using `Application::configure()->withMiddleware()`.
- `bootstrap/app.php` is the file to register middleware, exceptions, and routing files.
- `bootstrap/providers.php` contains application specific service providers.
- The `app\Console\Kernel.php` file no longer exists; use `bootstrap/app.php` or `routes/console.php` for console configuration.
- Console commands in `app/Console/Commands/` are automatically available and do not require manual registration.
## Database
- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost.
- Laravel 12 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`.
### Models
- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models.
=== pint/core rules ===
# Laravel Pint Code Formatter
- If you have modified any PHP files, you must run `vendor/bin/pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style.
- Do not run `vendor/bin/pint --test --format agent`, simply run `vendor/bin/pint --format agent` to fix any formatting issues.
=== pest/core rules ===
## Pest
- This project uses Pest for testing. Create tests: `php artisan make:test --pest {name}`.
- Run tests: `php artisan test --compact` or filter: `php artisan test --compact --filter=testName`.
- Do NOT delete tests without approval.
- CRITICAL: ALWAYS use `search-docs` tool for version-specific Pest documentation and updated code examples.
- IMPORTANT: Activate `pest-testing` every time you're working with a Pest or testing-related task.
</laravel-boost-guidelines>

25
TODO.md Normal file
View File

@@ -0,0 +1,25 @@
# Boğaziçi Platform - Entegrasyon Düzeltmeleri
## 🔴 KRİTİK
- [x] **1. `.env` dosyasını düzelt**`.env.example`'dan yeniden oluşturuldu, APP_KEY generate edildi
- [x] **2. Web form endpoint'ini düzelt** — Next.js API route `/api/basvuru/route.ts` oluşturuldu, FormData → JSON dönüşümü yapıp backend'e proxy'liyor
- [x] **3. Web form alan adlarını düzelt** — Tüm formlarda: `ad``name`, `course``target_course`, `education``education_level`, `source` hidden field + `kvkk_consent` checkbox eklendi
- [x] **4. LeadResource alan hatalarını düzelt**`utm` JSON parse, `consent_kvkk` mapping, `notes` mapping, `email` alanı eklendi
## 🟠 YÜKSEK ÖNCELİK
- [x] **5. Factory'lerde `order` → `order_index` düzelt** — FaqFactory.php, HeroSlideFactory.php
- [x] **6. Seeder'larda `order` → `order_index` düzelt** — FaqSeeder.php, HeroSlideSeeder.php, FaqContentSeeder.php
- [x] **7. CourseScheduleResource eksik alanları** — Migration, Model, Resource güncellendi: `instructor`, `enrolled_count`, `price_override`, `status`, `notes` eklendi
- [x] **8. Web'de kullanılmayan API endpoint'lerini entegre et**`api.ts`'e `getGuideCards()`, `getComments()`, `getSitemapData()` fonksiyonları eklendi
## 🟡 DÜŞÜK ÖNCELİK
- [x] **9. Admin'den çağrılmayan endpoint'ler**`apiResource` otomatik endpoint'leri, zararsız, atlandı
## ✅ DOĞRULAMA
- [x] **10. Testleri çalıştır ve geçir** — 2 test geçti
- [x] **11. Seed çalıştır ve doğrula** — Tüm seeder'lar sorunsuz çalıştı
- [x] **12. Web formlarını uçtan uca test et** — Lead API'ye curl ile tam form gönderildi, DB'ye doğru düştüğü ve LeadResource'un doğru döndüğü doğrulandı

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Actions\Announcement;
use App\DTOs\AnnouncementData;
use App\Events\ModelChanged;
use App\Models\Announcement;
use App\Repositories\Contracts\AnnouncementRepositoryInterface;
final class CreateAnnouncementAction
{
public function __construct(private AnnouncementRepositoryInterface $repository) {}
public function execute(AnnouncementData $data): Announcement
{
$result = $this->repository->create($data->toArray());
ModelChanged::dispatch(Announcement::class, 'created');
return $result;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Actions\Announcement;
use App\Events\ModelChanged;
use App\Models\Announcement;
use App\Repositories\Contracts\AnnouncementRepositoryInterface;
final class DeleteAnnouncementAction
{
public function __construct(private AnnouncementRepositoryInterface $repository) {}
public function execute(Announcement $announcement): bool
{
$this->repository->delete($announcement);
ModelChanged::dispatch(Announcement::class, 'deleted');
return true;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Actions\Announcement;
use App\DTOs\AnnouncementData;
use App\Events\ModelChanged;
use App\Models\Announcement;
use App\Repositories\Contracts\AnnouncementRepositoryInterface;
final class UpdateAnnouncementAction
{
public function __construct(private AnnouncementRepositoryInterface $repository) {}
public function execute(Announcement $announcement, AnnouncementData $data): Announcement
{
$result = $this->repository->update($announcement, $data->toArray());
ModelChanged::dispatch(Announcement::class, 'updated');
return $result;
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Actions\Category;
use App\DTOs\CategoryData;
use App\Events\ModelChanged;
use App\Models\Category;
use App\Repositories\Contracts\CategoryRepositoryInterface;
class CreateCategoryAction
{
public function __construct(private CategoryRepositoryInterface $repository) {}
public function execute(CategoryData $data): Category
{
/** @var Category */
$result = $this->repository->create($data->toArray());
ModelChanged::dispatch(Category::class, 'created');
return $result;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Actions\Category;
use App\Events\ModelChanged;
use App\Models\Category;
use App\Repositories\Contracts\CategoryRepositoryInterface;
class DeleteCategoryAction
{
public function __construct(private CategoryRepositoryInterface $repository) {}
public function execute(Category $category): bool
{
$this->repository->delete($category);
ModelChanged::dispatch(Category::class, 'deleted');
return true;
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Actions\Category;
use App\DTOs\CategoryData;
use App\Events\ModelChanged;
use App\Models\Category;
use App\Repositories\Contracts\CategoryRepositoryInterface;
class UpdateCategoryAction
{
public function __construct(private CategoryRepositoryInterface $repository) {}
public function execute(Category $category, CategoryData $data): Category
{
/** @var Category */
$result = $this->repository->update($category, $data->toArray());
ModelChanged::dispatch(Category::class, 'updated');
return $result;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Actions\Comment;
use App\DTOs\CommentData;
use App\Events\ModelChanged;
use App\Models\Comment;
use App\Repositories\Contracts\CommentRepositoryInterface;
final class CreateCommentAction
{
public function __construct(private CommentRepositoryInterface $repository) {}
public function execute(CommentData $data): Comment
{
$result = $this->repository->create($data->toArray());
ModelChanged::dispatch(Comment::class, 'created');
return $result;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Actions\Comment;
use App\Events\ModelChanged;
use App\Models\Comment;
use App\Repositories\Contracts\CommentRepositoryInterface;
final class DeleteCommentAction
{
public function __construct(private CommentRepositoryInterface $repository) {}
public function execute(Comment $comment): bool
{
$this->repository->delete($comment);
ModelChanged::dispatch(Comment::class, 'deleted');
return true;
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Actions\Comment;
use App\Events\ModelChanged;
use App\Models\Comment;
use App\Repositories\Contracts\CommentRepositoryInterface;
final class UpdateCommentAction
{
public function __construct(private CommentRepositoryInterface $repository) {}
/**
* @param array<string, mixed> $data
*/
public function execute(Comment $comment, array $data): Comment
{
$result = $this->repository->update($comment, $data);
ModelChanged::dispatch(Comment::class, 'updated');
return $result;
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Actions\Course;
use App\DTOs\CourseData;
use App\Events\ModelChanged;
use App\Models\Course;
use App\Repositories\Contracts\CourseRepositoryInterface;
class CreateCourseAction
{
public function __construct(private CourseRepositoryInterface $repository) {}
public function execute(CourseData $data): Course
{
/** @var Course */
$result = $this->repository->create($data->toArray());
ModelChanged::dispatch(Course::class, 'created');
return $result;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Actions\Course;
use App\Events\ModelChanged;
use App\Models\Course;
use App\Repositories\Contracts\CourseRepositoryInterface;
class DeleteCourseAction
{
public function __construct(private CourseRepositoryInterface $repository) {}
public function execute(Course $course): bool
{
$this->repository->delete($course);
ModelChanged::dispatch(Course::class, 'deleted');
return true;
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Actions\Course;
use App\DTOs\CourseData;
use App\Events\ModelChanged;
use App\Models\Course;
use App\Repositories\Contracts\CourseRepositoryInterface;
class UpdateCourseAction
{
public function __construct(private CourseRepositoryInterface $repository) {}
public function execute(Course $course, CourseData $data): Course
{
/** @var Course */
$result = $this->repository->update($course, $data->toArray());
ModelChanged::dispatch(Course::class, 'updated');
return $result;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Actions\Faq;
use App\DTOs\FaqData;
use App\Events\ModelChanged;
use App\Models\Faq;
use App\Repositories\Contracts\FaqRepositoryInterface;
final class CreateFaqAction
{
public function __construct(private FaqRepositoryInterface $repository) {}
public function execute(FaqData $data): Faq
{
$result = $this->repository->create($data->toArray());
ModelChanged::dispatch(Faq::class, 'created');
return $result;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Actions\Faq;
use App\Events\ModelChanged;
use App\Models\Faq;
use App\Repositories\Contracts\FaqRepositoryInterface;
final class DeleteFaqAction
{
public function __construct(private FaqRepositoryInterface $repository) {}
public function execute(Faq $faq): bool
{
$this->repository->delete($faq);
ModelChanged::dispatch(Faq::class, 'deleted');
return true;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Actions\Faq;
use App\DTOs\FaqData;
use App\Events\ModelChanged;
use App\Models\Faq;
use App\Repositories\Contracts\FaqRepositoryInterface;
final class UpdateFaqAction
{
public function __construct(private FaqRepositoryInterface $repository) {}
public function execute(Faq $faq, FaqData $data): Faq
{
$result = $this->repository->update($faq, $data->toArray());
ModelChanged::dispatch(Faq::class, 'updated');
return $result;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Actions\GuideCard;
use App\DTOs\GuideCardData;
use App\Events\ModelChanged;
use App\Models\GuideCard;
use App\Repositories\Contracts\GuideCardRepositoryInterface;
final class CreateGuideCardAction
{
public function __construct(private GuideCardRepositoryInterface $repository) {}
public function execute(GuideCardData $data): GuideCard
{
$result = $this->repository->create($data->toArray());
ModelChanged::dispatch(GuideCard::class, 'created');
return $result;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Actions\GuideCard;
use App\Events\ModelChanged;
use App\Models\GuideCard;
use App\Repositories\Contracts\GuideCardRepositoryInterface;
final class DeleteGuideCardAction
{
public function __construct(private GuideCardRepositoryInterface $repository) {}
public function execute(GuideCard $guideCard): bool
{
$this->repository->delete($guideCard);
ModelChanged::dispatch(GuideCard::class, 'deleted');
return true;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Actions\GuideCard;
use App\DTOs\GuideCardData;
use App\Events\ModelChanged;
use App\Models\GuideCard;
use App\Repositories\Contracts\GuideCardRepositoryInterface;
final class UpdateGuideCardAction
{
public function __construct(private GuideCardRepositoryInterface $repository) {}
public function execute(GuideCard $guideCard, GuideCardData $data): GuideCard
{
$result = $this->repository->update($guideCard, $data->toArray());
ModelChanged::dispatch(GuideCard::class, 'updated');
return $result;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Actions\HeroSlide;
use App\DTOs\HeroSlideData;
use App\Events\ModelChanged;
use App\Models\HeroSlide;
use App\Repositories\Contracts\HeroSlideRepositoryInterface;
final class CreateHeroSlideAction
{
public function __construct(private HeroSlideRepositoryInterface $repository) {}
public function execute(HeroSlideData $data): HeroSlide
{
$result = $this->repository->create($data->toArray());
ModelChanged::dispatch(HeroSlide::class, 'created');
return $result;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Actions\HeroSlide;
use App\Events\ModelChanged;
use App\Models\HeroSlide;
use App\Repositories\Contracts\HeroSlideRepositoryInterface;
final class DeleteHeroSlideAction
{
public function __construct(private HeroSlideRepositoryInterface $repository) {}
public function execute(HeroSlide $heroSlide): bool
{
$this->repository->delete($heroSlide);
ModelChanged::dispatch(HeroSlide::class, 'deleted');
return true;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Actions\HeroSlide;
use App\DTOs\HeroSlideData;
use App\Events\ModelChanged;
use App\Models\HeroSlide;
use App\Repositories\Contracts\HeroSlideRepositoryInterface;
final class UpdateHeroSlideAction
{
public function __construct(private HeroSlideRepositoryInterface $repository) {}
public function execute(HeroSlide $heroSlide, HeroSlideData $data): HeroSlide
{
$result = $this->repository->update($heroSlide, $data->toArray());
ModelChanged::dispatch(HeroSlide::class, 'updated');
return $result;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Actions\Lead;
use App\DTOs\LeadData;
use App\Events\ModelChanged;
use App\Models\Lead;
use App\Repositories\Contracts\LeadRepositoryInterface;
final class CreateLeadAction
{
public function __construct(private LeadRepositoryInterface $repository) {}
public function execute(LeadData $data): Lead
{
$result = $this->repository->create($data->toArray());
ModelChanged::dispatch(Lead::class, 'created');
return $result;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Actions\Lead;
use App\Events\ModelChanged;
use App\Models\Lead;
use App\Repositories\Contracts\LeadRepositoryInterface;
final class DeleteLeadAction
{
public function __construct(private LeadRepositoryInterface $repository) {}
public function execute(Lead $lead): bool
{
$this->repository->delete($lead);
ModelChanged::dispatch(Lead::class, 'deleted');
return true;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Actions\Lead;
use App\DTOs\LeadData;
use App\Events\ModelChanged;
use App\Models\Lead;
use App\Repositories\Contracts\LeadRepositoryInterface;
final class UpdateLeadAction
{
public function __construct(private LeadRepositoryInterface $repository) {}
public function execute(Lead $lead, LeadData $data): Lead
{
$result = $this->repository->update($lead, $data->toArray());
ModelChanged::dispatch(Lead::class, 'updated');
return $result;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Actions\Menu;
use App\DTOs\MenuData;
use App\Events\ModelChanged;
use App\Models\Menu;
use App\Repositories\Contracts\MenuRepositoryInterface;
final class CreateMenuAction
{
public function __construct(private MenuRepositoryInterface $repository) {}
public function execute(MenuData $data): Menu
{
$result = $this->repository->create($data->toArray());
ModelChanged::dispatch(Menu::class, 'created');
return $result;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Actions\Menu;
use App\Events\ModelChanged;
use App\Models\Menu;
use App\Repositories\Contracts\MenuRepositoryInterface;
final class DeleteMenuAction
{
public function __construct(private MenuRepositoryInterface $repository) {}
public function execute(Menu $menu): bool
{
$this->repository->delete($menu);
ModelChanged::dispatch(Menu::class, 'deleted');
return true;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Actions\Menu;
use App\DTOs\MenuData;
use App\Events\ModelChanged;
use App\Models\Menu;
use App\Repositories\Contracts\MenuRepositoryInterface;
final class UpdateMenuAction
{
public function __construct(private MenuRepositoryInterface $repository) {}
public function execute(Menu $menu, MenuData $data): Menu
{
$result = $this->repository->update($menu, $data->toArray());
ModelChanged::dispatch(Menu::class, 'updated');
return $result;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Actions\Page;
use App\DTOs\PageData;
use App\Events\ModelChanged;
use App\Models\Page;
use App\Repositories\Contracts\PageRepositoryInterface;
final class CreatePageAction
{
public function __construct(private PageRepositoryInterface $repository) {}
public function execute(PageData $data): Page
{
$result = $this->repository->create($data->toArray());
ModelChanged::dispatch(Page::class, 'created');
return $result;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Actions\Page;
use App\Events\ModelChanged;
use App\Models\Page;
use App\Repositories\Contracts\PageRepositoryInterface;
final class DeletePageAction
{
public function __construct(private PageRepositoryInterface $repository) {}
public function execute(Page $page): bool
{
$this->repository->delete($page);
ModelChanged::dispatch(Page::class, 'deleted');
return true;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Actions\Page;
use App\DTOs\PageData;
use App\Events\ModelChanged;
use App\Models\Page;
use App\Repositories\Contracts\PageRepositoryInterface;
final class UpdatePageAction
{
public function __construct(private PageRepositoryInterface $repository) {}
public function execute(Page $page, PageData $data): Page
{
$result = $this->repository->update($page, $data->toArray());
ModelChanged::dispatch(Page::class, 'updated');
return $result;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Actions\Schedule;
use App\DTOs\ScheduleData;
use App\Events\ModelChanged;
use App\Models\CourseSchedule;
use App\Repositories\Contracts\ScheduleRepositoryInterface;
final class CreateScheduleAction
{
public function __construct(private ScheduleRepositoryInterface $repository) {}
public function execute(ScheduleData $data): CourseSchedule
{
$result = $this->repository->create($data->toArray());
ModelChanged::dispatch(CourseSchedule::class, 'created');
return $result;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Actions\Schedule;
use App\Events\ModelChanged;
use App\Models\CourseSchedule;
use App\Repositories\Contracts\ScheduleRepositoryInterface;
final class DeleteScheduleAction
{
public function __construct(private ScheduleRepositoryInterface $repository) {}
public function execute(CourseSchedule $schedule): bool
{
$this->repository->delete($schedule);
ModelChanged::dispatch(CourseSchedule::class, 'deleted');
return true;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Actions\Schedule;
use App\DTOs\ScheduleData;
use App\Events\ModelChanged;
use App\Models\CourseSchedule;
use App\Repositories\Contracts\ScheduleRepositoryInterface;
final class UpdateScheduleAction
{
public function __construct(private ScheduleRepositoryInterface $repository) {}
public function execute(CourseSchedule $schedule, ScheduleData $data): CourseSchedule
{
$result = $this->repository->update($schedule, $data->toArray());
ModelChanged::dispatch(CourseSchedule::class, 'updated');
return $result;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Actions\Setting;
use App\Events\ModelChanged;
use App\Models\Setting;
use App\Repositories\Contracts\SettingRepositoryInterface;
final class UpdateSettingsAction
{
public function __construct(private SettingRepositoryInterface $repository) {}
/**
* @param array<string, mixed> $settings
*/
public function execute(array $settings): void
{
$this->repository->bulkUpdate($settings);
ModelChanged::dispatch(Setting::class, 'updated');
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Actions\User;
use App\DTOs\UserData;
use App\Events\ModelChanged;
use App\Models\User;
use App\Repositories\Contracts\UserRepositoryInterface;
class CreateUserAction
{
public function __construct(private UserRepositoryInterface $repository) {}
public function execute(UserData $data): User
{
/** @var User */
$user = $this->repository->create($data->toArray());
if ($data->role) {
$user->syncRoles([$data->role]);
}
$user->load('roles');
ModelChanged::dispatch(User::class, 'created');
return $user;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Actions\User;
use App\Events\ModelChanged;
use App\Models\User;
use App\Repositories\Contracts\UserRepositoryInterface;
class DeleteUserAction
{
public function __construct(private UserRepositoryInterface $repository) {}
public function execute(User $user): bool
{
$this->repository->delete($user);
ModelChanged::dispatch(User::class, 'deleted');
return true;
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Actions\User;
use App\DTOs\UserData;
use App\Events\ModelChanged;
use App\Models\User;
use App\Repositories\Contracts\UserRepositoryInterface;
class UpdateUserAction
{
public function __construct(private UserRepositoryInterface $repository) {}
public function execute(User $user, UserData $data): User
{
/** @var User */
$user = $this->repository->update($user, $data->toArray());
if ($data->role !== null) {
$user->syncRoles([$data->role]);
}
$user->load('roles');
ModelChanged::dispatch(User::class, 'updated');
return $user;
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\DTOs;
class AnnouncementData
{
public function __construct(
public readonly string $slug,
public readonly string $title,
public readonly string $category,
public readonly string $excerpt,
public readonly string $content,
public readonly ?string $image = null,
public readonly bool $isFeatured = false,
public readonly ?string $metaTitle = null,
public readonly ?string $metaDescription = null,
public readonly ?string $publishedAt = null,
) {}
/**
* @param array<string, mixed> $data
*/
public static function fromArray(array $data): self
{
return new self(
slug: $data['slug'],
title: $data['title'],
category: $data['category'],
excerpt: $data['excerpt'],
content: $data['content'],
image: $data['image'] ?? null,
isFeatured: $data['is_featured'] ?? false,
metaTitle: $data['meta_title'] ?? null,
metaDescription: $data['meta_description'] ?? null,
publishedAt: $data['published_at'] ?? null,
);
}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'slug' => $this->slug,
'title' => $this->title,
'category' => $this->category,
'excerpt' => $this->excerpt,
'content' => $this->content,
'image' => $this->image,
'is_featured' => $this->isFeatured,
'meta_title' => $this->metaTitle,
'meta_description' => $this->metaDescription,
'published_at' => $this->publishedAt,
];
}
}

45
app/DTOs/CategoryData.php Normal file
View File

@@ -0,0 +1,45 @@
<?php
namespace App\DTOs;
class CategoryData
{
public function __construct(
public readonly string $slug,
public readonly string $label,
public readonly ?string $desc = null,
public readonly ?string $image = null,
public readonly ?string $metaTitle = null,
public readonly ?string $metaDescription = null,
) {}
/**
* @param array<string, mixed> $data
*/
public static function fromArray(array $data): self
{
return new self(
slug: $data['slug'],
label: $data['label'],
desc: $data['desc'] ?? null,
image: $data['image'] ?? null,
metaTitle: $data['meta_title'] ?? null,
metaDescription: $data['meta_description'] ?? null,
);
}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'slug' => $this->slug,
'label' => $this->label,
'desc' => $this->desc,
'image' => $this->image,
'meta_title' => $this->metaTitle,
'meta_description' => $this->metaDescription,
];
}
}

48
app/DTOs/CommentData.php Normal file
View File

@@ -0,0 +1,48 @@
<?php
namespace App\DTOs;
final readonly class CommentData
{
public function __construct(
public string $commentableType,
public int $commentableId,
public string $authorName,
public string $content,
public ?int $rating,
public bool $isApproved = false,
public ?string $adminReply = null,
) {}
/**
* @param array<string, mixed> $data
*/
public static function fromArray(array $data): self
{
return new self(
commentableType: $data['commentable_type'],
commentableId: $data['commentable_id'],
content: $data['content'],
authorName: $data['author_name'],
rating: $data['rating'] ?? null,
isApproved: $data['is_approved'] ?? false,
adminReply: $data['admin_reply'] ?? null,
);
}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'commentable_type' => $this->commentableType,
'commentable_id' => $this->commentableId,
'author_name' => $this->authorName,
'content' => $this->content,
'rating' => $this->rating,
'is_approved' => $this->isApproved,
'admin_reply' => $this->adminReply,
];
}
}

92
app/DTOs/CourseData.php Normal file
View File

@@ -0,0 +1,92 @@
<?php
namespace App\DTOs;
class CourseData
{
/**
* @param list<string>|null $includes
* @param list<string>|null $requirements
* @param list<string>|null $scope
*/
public function __construct(
public readonly int $categoryId,
public readonly string $slug,
public readonly string $title,
public readonly string $desc,
public readonly string $longDesc,
public readonly string $duration,
public readonly ?string $sub = null,
public readonly int $students = 0,
public readonly float $rating = 5.0,
public readonly ?string $badge = null,
public readonly ?string $image = null,
public readonly ?string $price = null,
public readonly ?array $includes = null,
public readonly ?array $requirements = null,
public readonly ?string $metaTitle = null,
public readonly ?string $metaDescription = null,
public readonly ?array $scope = null,
public readonly ?string $standard = null,
public readonly ?string $language = null,
public readonly ?string $location = null,
) {}
/**
* @param array<string, mixed> $data
*/
public static function fromArray(array $data): self
{
return new self(
categoryId: $data['category_id'],
slug: $data['slug'],
title: $data['title'],
desc: $data['desc'],
longDesc: $data['long_desc'],
duration: $data['duration'],
sub: $data['sub'] ?? null,
students: $data['students'] ?? 0,
rating: $data['rating'] ?? 5.0,
badge: $data['badge'] ?? null,
image: $data['image'] ?? null,
price: $data['price'] ?? null,
includes: $data['includes'] ?? null,
requirements: $data['requirements'] ?? null,
metaTitle: $data['meta_title'] ?? null,
metaDescription: $data['meta_description'] ?? null,
scope: $data['scope'] ?? null,
standard: $data['standard'] ?? null,
language: $data['language'] ?? null,
location: $data['location'] ?? null,
);
}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'category_id' => $this->categoryId,
'slug' => $this->slug,
'title' => $this->title,
'sub' => $this->sub,
'desc' => $this->desc,
'long_desc' => $this->longDesc,
'duration' => $this->duration,
'students' => $this->students,
'rating' => $this->rating,
'badge' => $this->badge,
'image' => $this->image,
'price' => $this->price,
'includes' => $this->includes,
'requirements' => $this->requirements,
'meta_title' => $this->metaTitle,
'meta_description' => $this->metaDescription,
'scope' => $this->scope,
'standard' => $this->standard,
'language' => $this->language,
'location' => $this->location,
];
}
}

44
app/DTOs/FaqData.php Normal file
View File

@@ -0,0 +1,44 @@
<?php
namespace App\DTOs;
use App\Enums\FaqCategory;
final readonly class FaqData
{
public function __construct(
public string $question,
public string $answer,
public FaqCategory $category,
public int $orderIndex = 0,
public bool $isActive = true,
) {}
/**
* @param array<string, mixed> $data
*/
public static function fromArray(array $data): self
{
return new self(
question: $data['question'],
answer: $data['answer'],
category: FaqCategory::from($data['category']),
orderIndex: $data['order_index'] ?? 0,
isActive: $data['is_active'] ?? true,
);
}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'question' => $this->question,
'answer' => $this->answer,
'category' => $this->category->value,
'order_index' => $this->orderIndex,
'is_active' => $this->isActive,
];
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace App\DTOs;
final readonly class GuideCardData
{
public function __construct(
public string $title,
public string $description,
public ?string $icon,
public int $orderIndex = 0,
public bool $isActive = true,
) {}
/**
* @param array<string, mixed> $data
*/
public static function fromArray(array $data): self
{
return new self(
title: $data['title'],
description: $data['description'],
icon: $data['icon'] ?? null,
orderIndex: $data['order_index'] ?? 0,
isActive: $data['is_active'] ?? true,
);
}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'title' => $this->title,
'description' => $this->description,
'icon' => $this->icon,
'order_index' => $this->orderIndex,
'is_active' => $this->isActive,
];
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace App\DTOs;
final readonly class HeroSlideData
{
public function __construct(
public string $title,
public ?string $label = null,
public ?string $description = null,
public string $mediaType = 'image',
public ?string $image = null,
public ?string $videoUrl = null,
public ?string $mobileVideoUrl = null,
public ?string $mobileImage = null,
public ?string $buttonText = null,
public ?string $buttonUrl = null,
public int $orderIndex = 0,
public bool $isActive = true,
) {}
/**
* @param array<string, mixed> $data
*/
public static function fromArray(array $data): self
{
return new self(
title: $data['title'],
label: $data['label'] ?? null,
description: $data['description'] ?? null,
mediaType: $data['media_type'] ?? 'image',
image: $data['image'] ?? null,
videoUrl: $data['video_url'] ?? null,
mobileVideoUrl: $data['mobile_video_url'] ?? null,
mobileImage: $data['mobile_image'] ?? null,
buttonText: $data['button_text'] ?? null,
buttonUrl: $data['button_url'] ?? null,
orderIndex: $data['order_index'] ?? 0,
isActive: $data['is_active'] ?? true,
);
}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'label' => $this->label,
'title' => $this->title,
'description' => $this->description,
'media_type' => $this->mediaType,
'image' => $this->image,
'video_url' => $this->videoUrl,
'mobile_video_url' => $this->mobileVideoUrl,
'mobile_image' => $this->mobileImage,
'button_text' => $this->buttonText,
'button_url' => $this->buttonUrl,
'order_index' => $this->orderIndex,
'is_active' => $this->isActive,
];
}
}

69
app/DTOs/LeadData.php Normal file
View File

@@ -0,0 +1,69 @@
<?php
namespace App\DTOs;
class LeadData
{
public function __construct(
public readonly string $name,
public readonly string $phone,
public readonly string $source,
public readonly ?string $email = null,
public readonly ?string $targetCourse = null,
public readonly ?string $educationLevel = null,
public readonly ?string $subject = null,
public readonly ?string $message = null,
public readonly ?array $utm = null,
public readonly bool $consentKvkk = false,
public readonly bool $marketingConsent = false,
public readonly ?string $consentTextVersion = null,
) {}
/**
* @param array<string, mixed> $data
*/
public static function fromArray(array $data): self
{
$utm = array_filter([
'utm_source' => $data['utm_source'] ?? null,
'utm_medium' => $data['utm_medium'] ?? null,
'utm_campaign' => $data['utm_campaign'] ?? null,
]);
return new self(
name: $data['name'],
phone: $data['phone'],
source: $data['source'],
email: $data['email'] ?? null,
targetCourse: $data['target_course'] ?? null,
educationLevel: $data['education_level'] ?? null,
subject: $data['subject'] ?? null,
message: $data['message'] ?? null,
utm: $utm ?: null,
consentKvkk: (bool) ($data['kvkk_consent'] ?? false),
marketingConsent: (bool) ($data['marketing_consent'] ?? false),
consentTextVersion: $data['consent_text_version'] ?? null,
);
}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'name' => $this->name,
'phone' => $this->phone,
'email' => $this->email,
'source' => $this->source,
'target_course' => $this->targetCourse,
'education_level' => $this->educationLevel,
'subject' => $this->subject,
'message' => $this->message,
'utm' => $this->utm,
'consent_kvkk' => $this->consentKvkk,
'marketing_consent' => $this->marketingConsent,
'consent_text_version' => $this->consentTextVersion,
];
}
}

51
app/DTOs/MenuData.php Normal file
View File

@@ -0,0 +1,51 @@
<?php
namespace App\DTOs;
use App\Enums\MenuLocation;
use App\Enums\MenuType;
final readonly class MenuData
{
public function __construct(
public string $label,
public string $url,
public MenuLocation $location,
public MenuType $type,
public ?int $parentId,
public int $orderIndex = 0,
public bool $isActive = true,
) {}
/**
* @param array<string, mixed> $data
*/
public static function fromArray(array $data): self
{
return new self(
label: $data['label'],
url: $data['url'],
location: MenuLocation::from($data['location']),
type: MenuType::from($data['type']),
parentId: $data['parent_id'] ?? null,
orderIndex: $data['order_index'] ?? 0,
isActive: $data['is_active'] ?? true,
);
}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'label' => $this->label,
'url' => $this->url,
'location' => $this->location->value,
'type' => $this->type->value,
'parent_id' => $this->parentId,
'order_index' => $this->orderIndex,
'is_active' => $this->isActive,
];
}
}

45
app/DTOs/PageData.php Normal file
View File

@@ -0,0 +1,45 @@
<?php
namespace App\DTOs;
final readonly class PageData
{
public function __construct(
public string $slug,
public string $title,
public ?string $content,
public ?string $metaTitle,
public ?string $metaDescription,
public bool $isActive = true,
) {}
/**
* @param array<string, mixed> $data
*/
public static function fromArray(array $data): self
{
return new self(
slug: $data['slug'],
title: $data['title'],
content: $data['content'] ?? null,
metaTitle: $data['meta_title'] ?? null,
metaDescription: $data['meta_description'] ?? null,
isActive: $data['is_active'] ?? true,
);
}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'slug' => $this->slug,
'title' => $this->title,
'content' => $this->content,
'meta_title' => $this->metaTitle,
'meta_description' => $this->metaDescription,
'is_active' => $this->isActive,
];
}
}

48
app/DTOs/ScheduleData.php Normal file
View File

@@ -0,0 +1,48 @@
<?php
namespace App\DTOs;
class ScheduleData
{
public function __construct(
public readonly int $courseId,
public readonly string $startDate,
public readonly string $endDate,
public readonly string $location,
public readonly int $quota,
public readonly int $availableSeats,
public readonly bool $isUrgent = false,
) {}
/**
* @param array<string, mixed> $data
*/
public static function fromArray(array $data): self
{
return new self(
courseId: $data['course_id'],
startDate: $data['start_date'],
endDate: $data['end_date'],
location: $data['location'],
quota: $data['quota'],
availableSeats: $data['available_seats'],
isUrgent: $data['is_urgent'] ?? false,
);
}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'course_id' => $this->courseId,
'start_date' => $this->startDate,
'end_date' => $this->endDate,
'location' => $this->location,
'quota' => $this->quota,
'available_seats' => $this->availableSeats,
'is_urgent' => $this->isUrgent,
];
}
}

43
app/DTOs/UserData.php Normal file
View File

@@ -0,0 +1,43 @@
<?php
namespace App\DTOs;
class UserData
{
public function __construct(
public readonly string $name,
public readonly string $email,
public readonly ?string $password = null,
public readonly ?string $role = null,
) {}
/**
* @param array<string, mixed> $data
*/
public static function fromArray(array $data): self
{
return new self(
name: $data['name'],
email: $data['email'],
password: $data['password'] ?? null,
role: $data['role'] ?? null,
);
}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
$result = [
'name' => $this->name,
'email' => $this->email,
];
if ($this->password !== null) {
$result['password'] = $this->password;
}
return $result;
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Enums;
enum AnnouncementCategory: string
{
case Announcement = 'announcement';
case News = 'news';
case Event = 'event';
}

23
app/Enums/CourseBadge.php Normal file
View File

@@ -0,0 +1,23 @@
<?php
namespace App\Enums;
enum CourseBadge: string
{
case Popular = 'popular';
case MostPreferred = 'most_preferred';
case New = 'new';
case Recommended = 'recommended';
case Limited = 'limited';
public function label(): string
{
return match ($this) {
self::Popular => 'Popüler',
self::MostPreferred => 'En Çok Tercih Edilen',
self::New => 'Yeni',
self::Recommended => 'Önerilen',
self::Limited => 'Sınırlı Kontenjan',
};
}
}

15
app/Enums/FaqCategory.php Normal file
View File

@@ -0,0 +1,15 @@
<?php
namespace App\Enums;
enum FaqCategory: string
{
case Egitimler = 'egitimler';
case Stcw = 'stcw';
case Makine = 'makine';
case YatKaptanligi = 'yat-kaptanligi';
case Yenileme = 'yenileme';
case Guvenlik = 'guvenlik';
case Kayit = 'kayit';
case Iletisim = 'iletisim';
}

13
app/Enums/LeadSource.php Normal file
View File

@@ -0,0 +1,13 @@
<?php
namespace App\Enums;
enum LeadSource: string
{
case KursKayit = 'kurs_kayit';
case Danismanlik = 'danismanlik';
case Duyuru = 'duyuru';
case Iletisim = 'iletisim';
case HeroForm = 'hero_form';
case WhatsappWidget = 'whatsapp_widget';
}

11
app/Enums/LeadStatus.php Normal file
View File

@@ -0,0 +1,11 @@
<?php
namespace App\Enums;
enum LeadStatus: string
{
case New = 'new';
case Contacted = 'contacted';
case Enrolled = 'enrolled';
case Cancelled = 'cancelled';
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Enums;
enum MenuLocation: string
{
case HeaderMain = 'header_main';
case FooterCorporate = 'footer_corporate';
case FooterEducation = 'footer_education';
case FooterQuicklinks = 'footer_quicklinks';
}

10
app/Enums/MenuType.php Normal file
View File

@@ -0,0 +1,10 @@
<?php
namespace App\Enums;
enum MenuType: string
{
case Link = 'link';
case MegaMenuEducation = 'mega_menu_education';
case MegaMenuCalendar = 'mega_menu_calendar';
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Enums;
enum SettingGroup: string
{
case General = 'general';
case Contact = 'contact';
case Maps = 'maps';
case Social = 'social';
case Seo = 'seo';
case Analytics = 'analytics';
case Header = 'header';
case Footer = 'footer';
case Integrations = 'integrations';
case InfoSections = 'info_sections';
}

15
app/Enums/SettingType.php Normal file
View File

@@ -0,0 +1,15 @@
<?php
namespace App\Enums;
enum SettingType: string
{
case Text = 'text';
case Textarea = 'textarea';
case Image = 'image';
case Boolean = 'boolean';
case Json = 'json';
case Richtext = 'richtext';
case Url = 'url';
case Color = 'color';
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class ModelChanged
{
use Dispatchable, SerializesModels;
/**
* Create a new event instance.
*
* @param string $modelClass The fully qualified class name of the changed model.
* @param string $action The action performed (created, updated, deleted).
*/
public function __construct(
public string $modelClass,
public string $action = 'updated',
) {}
}

View File

@@ -0,0 +1,140 @@
<?php
namespace App\Http\Controllers\Api\Admin;
use App\Actions\Announcement\CreateAnnouncementAction;
use App\Actions\Announcement\DeleteAnnouncementAction;
use App\Actions\Announcement\UpdateAnnouncementAction;
use App\DTOs\AnnouncementData;
use App\Http\Controllers\Controller;
use App\Http\Requests\Announcement\StoreAnnouncementRequest;
use App\Http\Requests\Announcement\UpdateAnnouncementRequest;
use App\Http\Resources\AnnouncementResource;
use App\Models\Announcement;
use App\Repositories\Contracts\AnnouncementRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use OpenApi\Attributes as OA;
class AnnouncementController extends Controller
{
public function __construct(private AnnouncementRepositoryInterface $repository) {}
#[OA\Get(
path: '/api/admin/announcements',
summary: 'Duyuruları listele (Admin)',
tags: ['Admin - Announcements'],
security: [['sanctum' => []]],
parameters: [
new OA\Parameter(name: 'category', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'featured', in: 'query', required: false, schema: new OA\Schema(type: 'boolean')),
new OA\Parameter(name: 'search', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'sort', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'per_page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
],
responses: [new OA\Response(response: 200, description: 'Duyuru listesi')],
)]
public function index(Request $request): AnonymousResourceCollection
{
$announcements = $this->repository->paginate(
$request->only(['category', 'featured', 'search', 'sort']),
$request->integer('per_page', 15),
);
return AnnouncementResource::collection($announcements);
}
#[OA\Post(
path: '/api/admin/announcements',
summary: 'Yeni duyuru oluştur',
tags: ['Admin - Announcements'],
security: [['sanctum' => []]],
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
required: ['title', 'slug', 'category', 'content'],
properties: [
new OA\Property(property: 'title', type: 'string'),
new OA\Property(property: 'slug', type: 'string'),
new OA\Property(property: 'category', type: 'string'),
new OA\Property(property: 'content', type: 'string'),
new OA\Property(property: 'excerpt', type: 'string'),
new OA\Property(property: 'image', type: 'string'),
new OA\Property(property: 'is_featured', type: 'boolean'),
new OA\Property(property: 'is_active', type: 'boolean'),
new OA\Property(property: 'published_at', type: 'string', format: 'date-time'),
new OA\Property(property: 'meta_title', type: 'string'),
new OA\Property(property: 'meta_description', type: 'string'),
],
)),
responses: [
new OA\Response(response: 201, description: 'Duyuru oluşturuldu'),
new OA\Response(response: 422, description: 'Validasyon hatası'),
],
)]
public function store(StoreAnnouncementRequest $request, CreateAnnouncementAction $action): JsonResponse
{
$dto = AnnouncementData::fromArray($request->validated());
$announcement = $action->execute($dto);
return response()->json(new AnnouncementResource($announcement), 201);
}
#[OA\Get(
path: '/api/admin/announcements/{announcement}',
summary: 'Duyuru detayı (Admin)',
tags: ['Admin - Announcements'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'announcement', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
responses: [new OA\Response(response: 200, description: 'Duyuru detayı')],
)]
public function show(Announcement $announcement): JsonResponse
{
return response()->json(new AnnouncementResource($announcement));
}
#[OA\Put(
path: '/api/admin/announcements/{announcement}',
summary: 'Duyuru güncelle',
tags: ['Admin - Announcements'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'announcement', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
properties: [
new OA\Property(property: 'title', type: 'string'),
new OA\Property(property: 'slug', type: 'string'),
new OA\Property(property: 'category', type: 'string'),
new OA\Property(property: 'content', type: 'string'),
new OA\Property(property: 'excerpt', type: 'string'),
new OA\Property(property: 'image', type: 'string'),
new OA\Property(property: 'is_featured', type: 'boolean'),
new OA\Property(property: 'is_active', type: 'boolean'),
],
)),
responses: [
new OA\Response(response: 200, description: 'Duyuru güncellendi'),
new OA\Response(response: 422, description: 'Validasyon hatası'),
],
)]
public function update(UpdateAnnouncementRequest $request, Announcement $announcement, UpdateAnnouncementAction $action): JsonResponse
{
$dto = AnnouncementData::fromArray(array_merge($announcement->toArray(), $request->validated()));
$announcement = $action->execute($announcement, $dto);
return response()->json(new AnnouncementResource($announcement));
}
#[OA\Delete(
path: '/api/admin/announcements/{announcement}',
summary: 'Duyuru sil',
tags: ['Admin - Announcements'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'announcement', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
responses: [new OA\Response(response: 200, description: 'Duyuru silindi')],
)]
public function destroy(Announcement $announcement, DeleteAnnouncementAction $action): JsonResponse
{
$action->execute($announcement);
return response()->json(['message' => 'Duyuru silindi.']);
}
}

View File

@@ -0,0 +1,118 @@
<?php
namespace App\Http\Controllers\Api\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use OpenApi\Attributes as OA;
class AuthController extends Controller
{
#[OA\Post(
path: '/api/admin/login',
summary: 'Admin girişi',
description: 'E-posta ve şifre ile giriş yaparak Sanctum token alır.',
tags: ['Auth'],
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(
required: ['email', 'password'],
properties: [
new OA\Property(property: 'email', type: 'string', format: 'email', example: 'admin@bogazicidenizcilik.com.tr'),
new OA\Property(property: 'password', type: 'string', format: 'password', example: 'password'),
],
),
),
responses: [
new OA\Response(response: 200, description: 'Başarılı giriş', content: new OA\JsonContent(
properties: [
new OA\Property(property: 'data', type: 'object', properties: [
new OA\Property(property: 'token', type: 'string'),
new OA\Property(property: 'user', type: 'object'),
]),
],
)),
new OA\Response(response: 401, description: 'Geçersiz kimlik bilgileri'),
new OA\Response(response: 422, description: 'Validasyon hatası'),
],
)]
public function login(LoginRequest $request): JsonResponse
{
if (! Auth::attempt($request->only('email', 'password'))) {
return response()->json([
'message' => 'Geçersiz e-posta veya şifre.',
], 401);
}
/** @var User $user */
$user = Auth::user();
$token = $user->createToken('admin-token')->plainTextToken;
return response()->json([
'data' => [
'token' => $token,
'user' => [
'id' => $user->id,
'name' => $user->name,
'email' => $user->email,
'roles' => $user->getRoleNames(),
'permissions' => $user->getAllPermissions()->pluck('name'),
],
],
]);
}
#[OA\Get(
path: '/api/admin/me',
summary: 'Mevcut kullanıcı bilgileri',
description: 'Oturum açmış kullanıcının bilgilerini, rollerini ve izinlerini döndürür.',
security: [['sanctum' => []]],
tags: ['Auth'],
responses: [
new OA\Response(response: 200, description: 'Kullanıcı bilgileri'),
new OA\Response(response: 401, description: 'Yetkisiz erişim'),
],
)]
public function me(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
return response()->json([
'data' => [
'id' => $user->id,
'name' => $user->name,
'email' => $user->email,
'roles' => $user->getRoleNames(),
'permissions' => $user->getAllPermissions()->pluck('name'),
],
]);
}
#[OA\Post(
path: '/api/admin/logout',
summary: ıkış yap',
description: 'Mevcut token\'ı iptal eder.',
security: [['sanctum' => []]],
tags: ['Auth'],
responses: [
new OA\Response(response: 200, description: 'Başarıyla çıkış yapıldı'),
new OA\Response(response: 401, description: 'Yetkisiz erişim'),
],
)]
public function logout(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$user->currentAccessToken()->delete();
return response()->json([
'message' => 'Başarıyla çıkış yapıldı.',
]);
}
}

View File

@@ -0,0 +1,172 @@
<?php
namespace App\Http\Controllers\Api\Admin;
use App\Http\Controllers\Controller;
use App\Http\Resources\PageBlockResource;
use App\Models\Page;
use App\Models\PageBlock;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use OpenApi\Attributes as OA;
class BlockController extends Controller
{
#[OA\Get(
path: '/api/admin/pages/{page}/blocks',
summary: 'Sayfa bloklarını listele',
tags: ['Admin - Page Blocks'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'page', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
responses: [new OA\Response(response: 200, description: 'Blok listesi')],
)]
public function index(Page $page): AnonymousResourceCollection
{
return PageBlockResource::collection(
$page->blocks()->orderBy('order_index')->get()
);
}
#[OA\Post(
path: '/api/admin/pages/{page}/blocks',
summary: 'Yeni blok oluştur',
tags: ['Admin - Page Blocks'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'page', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
required: ['type', 'content'],
properties: [
new OA\Property(property: 'type', type: 'string'),
new OA\Property(property: 'content', type: 'object'),
new OA\Property(property: 'order_index', type: 'integer'),
new OA\Property(property: 'is_active', type: 'boolean'),
],
)),
responses: [
new OA\Response(response: 201, description: 'Blok oluşturuldu'),
new OA\Response(response: 422, description: 'Validasyon hatası'),
],
)]
public function store(Request $request, Page $page): JsonResponse
{
$validated = $request->validate([
'type' => ['required', 'string', 'max:50'],
'content' => ['present', 'array'],
'order_index' => ['sometimes', 'integer', 'min:0'],
'is_active' => ['sometimes', 'boolean'],
]);
$validated['order_index'] ??= $page->blocks()->max('order_index') + 1;
$block = $page->blocks()->create($validated);
return response()->json(new PageBlockResource($block), 201);
}
#[OA\Get(
path: '/api/admin/pages/{page}/blocks/{block}',
summary: 'Blok detayı',
tags: ['Admin - Page Blocks'],
security: [['sanctum' => []]],
parameters: [
new OA\Parameter(name: 'page', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
new OA\Parameter(name: 'block', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
],
responses: [new OA\Response(response: 200, description: 'Blok detayı')],
)]
public function show(Page $page, PageBlock $block): JsonResponse
{
return response()->json(new PageBlockResource($block));
}
#[OA\Put(
path: '/api/admin/pages/{page}/blocks/{block}',
summary: 'Blok güncelle',
tags: ['Admin - Page Blocks'],
security: [['sanctum' => []]],
parameters: [
new OA\Parameter(name: 'page', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
new OA\Parameter(name: 'block', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
],
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
properties: [
new OA\Property(property: 'type', type: 'string'),
new OA\Property(property: 'content', type: 'object'),
new OA\Property(property: 'order_index', type: 'integer'),
new OA\Property(property: 'is_active', type: 'boolean'),
],
)),
responses: [
new OA\Response(response: 200, description: 'Blok güncellendi'),
new OA\Response(response: 422, description: 'Validasyon hatası'),
],
)]
public function update(Request $request, Page $page, PageBlock $block): JsonResponse
{
$validated = $request->validate([
'type' => ['sometimes', 'string', 'max:50'],
'content' => ['sometimes', 'array'],
'order_index' => ['sometimes', 'integer', 'min:0'],
'is_active' => ['sometimes', 'boolean'],
]);
$block->update($validated);
return response()->json(new PageBlockResource($block->fresh()));
}
#[OA\Delete(
path: '/api/admin/pages/{page}/blocks/{block}',
summary: 'Blok sil',
tags: ['Admin - Page Blocks'],
security: [['sanctum' => []]],
parameters: [
new OA\Parameter(name: 'page', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
new OA\Parameter(name: 'block', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
],
responses: [new OA\Response(response: 200, description: 'Blok silindi')],
)]
public function destroy(Page $page, PageBlock $block): JsonResponse
{
$block->delete();
return response()->json(['message' => 'Blok silindi.']);
}
#[OA\Post(
path: '/api/admin/pages/{page}/blocks/reorder',
summary: 'Blok sıralamasını güncelle',
tags: ['Admin - Page Blocks'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'page', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
required: ['items'],
properties: [
new OA\Property(property: 'items', type: 'array', items: new OA\Items(
properties: [
new OA\Property(property: 'id', type: 'integer'),
new OA\Property(property: 'order_index', type: 'integer'),
],
)),
],
)),
responses: [new OA\Response(response: 200, description: 'Sıralama güncellendi')],
)]
public function reorder(Request $request, Page $page): JsonResponse
{
$validated = $request->validate([
'items' => ['required', 'array', 'min:1'],
'items.*.id' => ['required', 'integer', 'exists:page_blocks,id'],
'items.*.order_index' => ['required', 'integer', 'min:0'],
]);
foreach ($validated['items'] as $item) {
$page->blocks()
->where('id', $item['id'])
->update(['order_index' => $item['order_index']]);
}
return response()->json(['message' => 'Blok sıralaması güncellendi.']);
}
}

View File

@@ -0,0 +1,147 @@
<?php
namespace App\Http\Controllers\Api\Admin;
use App\Actions\Category\CreateCategoryAction;
use App\Actions\Category\DeleteCategoryAction;
use App\Actions\Category\UpdateCategoryAction;
use App\DTOs\CategoryData;
use App\Http\Controllers\Controller;
use App\Http\Requests\Category\StoreCategoryRequest;
use App\Http\Requests\Category\UpdateCategoryRequest;
use App\Http\Resources\CategoryResource;
use App\Models\Category;
use App\Repositories\Contracts\CategoryRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use OpenApi\Attributes as OA;
class CategoryController extends Controller
{
public function __construct(private CategoryRepositoryInterface $repository) {}
#[OA\Get(
path: '/api/admin/categories',
summary: 'Kategorileri listele (Admin)',
tags: ['Admin - Categories'],
security: [['sanctum' => []]],
parameters: [
new OA\Parameter(name: 'search', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'per_page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
],
responses: [new OA\Response(response: 200, description: 'Kategori listesi')],
)]
public function index(Request $request): AnonymousResourceCollection
{
$categories = $this->repository->paginate(
filters: $request->only('search'),
perPage: $request->integer('per_page', 15),
);
return CategoryResource::collection($categories);
}
#[OA\Post(
path: '/api/admin/categories',
summary: 'Yeni kategori oluştur',
tags: ['Admin - Categories'],
security: [['sanctum' => []]],
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(
required: ['name'],
properties: [
new OA\Property(property: 'name', type: 'string'),
new OA\Property(property: 'slug', type: 'string'),
new OA\Property(property: 'description', type: 'string'),
new OA\Property(property: 'image', type: 'string'),
new OA\Property(property: 'icon', type: 'string'),
new OA\Property(property: 'is_active', type: 'boolean'),
new OA\Property(property: 'sort_order', type: 'integer'),
new OA\Property(property: 'meta_title', type: 'string'),
new OA\Property(property: 'meta_description', type: 'string'),
],
),
),
responses: [
new OA\Response(response: 201, description: 'Kategori oluşturuldu'),
new OA\Response(response: 422, description: 'Validasyon hatası'),
],
)]
public function store(StoreCategoryRequest $request, CreateCategoryAction $action): JsonResponse
{
$dto = CategoryData::fromArray($request->validated());
$category = $action->execute($dto);
return (new CategoryResource($category))
->response()
->setStatusCode(201);
}
#[OA\Get(
path: '/api/admin/categories/{category}',
summary: 'Kategori detayı (Admin)',
tags: ['Admin - Categories'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'category', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
responses: [
new OA\Response(response: 200, description: 'Kategori detayı'),
new OA\Response(response: 404, description: 'Bulunamadı'),
],
)]
public function show(Category $category): CategoryResource
{
return new CategoryResource($category);
}
#[OA\Put(
path: '/api/admin/categories/{category}',
summary: 'Kategori güncelle',
tags: ['Admin - Categories'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'category', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
properties: [
new OA\Property(property: 'name', type: 'string'),
new OA\Property(property: 'slug', type: 'string'),
new OA\Property(property: 'description', type: 'string'),
new OA\Property(property: 'image', type: 'string'),
new OA\Property(property: 'icon', type: 'string'),
new OA\Property(property: 'is_active', type: 'boolean'),
new OA\Property(property: 'sort_order', type: 'integer'),
new OA\Property(property: 'meta_title', type: 'string'),
new OA\Property(property: 'meta_description', type: 'string'),
],
)),
responses: [
new OA\Response(response: 200, description: 'Kategori güncellendi'),
new OA\Response(response: 422, description: 'Validasyon hatası'),
],
)]
public function update(UpdateCategoryRequest $request, Category $category, UpdateCategoryAction $action): CategoryResource
{
$dto = CategoryData::fromArray($request->validated());
$category = $action->execute($category, $dto);
return new CategoryResource($category);
}
#[OA\Delete(
path: '/api/admin/categories/{category}',
summary: 'Kategori sil',
tags: ['Admin - Categories'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'category', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
responses: [
new OA\Response(response: 200, description: 'Kategori silindi'),
new OA\Response(response: 404, description: 'Bulunamadı'),
],
)]
public function destroy(Category $category, DeleteCategoryAction $action): JsonResponse
{
$action->execute($category);
return response()->json(['message' => 'Kategori başarıyla silindi.']);
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace App\Http\Controllers\Api\Admin;
use App\Actions\Comment\DeleteCommentAction;
use App\Actions\Comment\UpdateCommentAction;
use App\Http\Controllers\Controller;
use App\Http\Requests\Comment\UpdateCommentRequest;
use App\Http\Resources\CommentResource;
use App\Models\Comment;
use App\Repositories\Contracts\CommentRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use OpenApi\Attributes as OA;
class CommentController extends Controller
{
public function __construct(private CommentRepositoryInterface $repository) {}
#[OA\Get(
path: '/api/admin/comments',
summary: 'Yorumları listele (Admin)',
tags: ['Admin - Comments'],
security: [['sanctum' => []]],
parameters: [
new OA\Parameter(name: 'is_approved', in: 'query', required: false, schema: new OA\Schema(type: 'boolean')),
new OA\Parameter(name: 'commentable_type', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'search', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'per_page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
],
responses: [new OA\Response(response: 200, description: 'Yorum listesi')],
)]
public function index(Request $request): AnonymousResourceCollection
{
$comments = $this->repository->paginate(
$request->only(['is_approved', 'commentable_type', 'search']),
$request->integer('per_page', 15),
);
return CommentResource::collection($comments);
}
#[OA\Get(
path: '/api/admin/comments/{comment}',
summary: 'Yorum detayı',
tags: ['Admin - Comments'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'comment', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
responses: [new OA\Response(response: 200, description: 'Yorum detayı')],
)]
public function show(Comment $comment): JsonResponse
{
return response()->json(new CommentResource($comment));
}
#[OA\Put(
path: '/api/admin/comments/{comment}',
summary: 'Yorum güncelle (onayla/reddet)',
tags: ['Admin - Comments'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'comment', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
properties: [
new OA\Property(property: 'is_approved', type: 'boolean'),
],
)),
responses: [
new OA\Response(response: 200, description: 'Yorum güncellendi'),
new OA\Response(response: 422, description: 'Validasyon hatası'),
],
)]
public function update(UpdateCommentRequest $request, Comment $comment, UpdateCommentAction $action): JsonResponse
{
$comment = $action->execute($comment, $request->validated());
return response()->json(new CommentResource($comment));
}
#[OA\Delete(
path: '/api/admin/comments/{comment}',
summary: 'Yorum sil',
tags: ['Admin - Comments'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'comment', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
responses: [new OA\Response(response: 200, description: 'Yorum silindi')],
)]
public function destroy(Comment $comment, DeleteCommentAction $action): JsonResponse
{
$action->execute($comment);
return response()->json(['message' => 'Yorum silindi.']);
}
}

View File

@@ -0,0 +1,172 @@
<?php
namespace App\Http\Controllers\Api\Admin;
use App\Http\Controllers\Controller;
use App\Http\Resources\CourseBlockResource;
use App\Models\Course;
use App\Models\CourseBlock;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use OpenApi\Attributes as OA;
class CourseBlockController extends Controller
{
#[OA\Get(
path: '/api/admin/courses/{course}/blocks',
summary: 'Eğitim bloklarını listele',
tags: ['Admin - Course Blocks'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'course', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
responses: [new OA\Response(response: 200, description: 'Blok listesi')],
)]
public function index(Course $course): AnonymousResourceCollection
{
return CourseBlockResource::collection(
$course->blocks()->orderBy('order_index')->get()
);
}
#[OA\Post(
path: '/api/admin/courses/{course}/blocks',
summary: 'Yeni eğitim bloğu oluştur',
tags: ['Admin - Course Blocks'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'course', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
required: ['type', 'content'],
properties: [
new OA\Property(property: 'type', type: 'string'),
new OA\Property(property: 'content', type: 'object'),
new OA\Property(property: 'order_index', type: 'integer'),
new OA\Property(property: 'is_active', type: 'boolean'),
],
)),
responses: [
new OA\Response(response: 201, description: 'Blok oluşturuldu'),
new OA\Response(response: 422, description: 'Validasyon hatası'),
],
)]
public function store(Request $request, Course $course): JsonResponse
{
$validated = $request->validate([
'type' => ['required', 'string', 'max:50'],
'content' => ['present', 'array'],
'order_index' => ['sometimes', 'integer', 'min:0'],
'is_active' => ['sometimes', 'boolean'],
]);
$validated['order_index'] ??= $course->blocks()->max('order_index') + 1;
$block = $course->blocks()->create($validated);
return response()->json(new CourseBlockResource($block), 201);
}
#[OA\Get(
path: '/api/admin/courses/{course}/blocks/{block}',
summary: 'Eğitim blok detayı',
tags: ['Admin - Course Blocks'],
security: [['sanctum' => []]],
parameters: [
new OA\Parameter(name: 'course', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
new OA\Parameter(name: 'block', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
],
responses: [new OA\Response(response: 200, description: 'Blok detayı')],
)]
public function show(Course $course, CourseBlock $block): JsonResponse
{
return response()->json(new CourseBlockResource($block));
}
#[OA\Put(
path: '/api/admin/courses/{course}/blocks/{block}',
summary: 'Eğitim bloğu güncelle',
tags: ['Admin - Course Blocks'],
security: [['sanctum' => []]],
parameters: [
new OA\Parameter(name: 'course', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
new OA\Parameter(name: 'block', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
],
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
properties: [
new OA\Property(property: 'type', type: 'string'),
new OA\Property(property: 'content', type: 'object'),
new OA\Property(property: 'order_index', type: 'integer'),
new OA\Property(property: 'is_active', type: 'boolean'),
],
)),
responses: [
new OA\Response(response: 200, description: 'Blok güncellendi'),
new OA\Response(response: 422, description: 'Validasyon hatası'),
],
)]
public function update(Request $request, Course $course, CourseBlock $block): JsonResponse
{
$validated = $request->validate([
'type' => ['sometimes', 'string', 'max:50'],
'content' => ['sometimes', 'array'],
'order_index' => ['sometimes', 'integer', 'min:0'],
'is_active' => ['sometimes', 'boolean'],
]);
$block->update($validated);
return response()->json(new CourseBlockResource($block->fresh()));
}
#[OA\Delete(
path: '/api/admin/courses/{course}/blocks/{block}',
summary: 'Eğitim bloğu sil',
tags: ['Admin - Course Blocks'],
security: [['sanctum' => []]],
parameters: [
new OA\Parameter(name: 'course', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
new OA\Parameter(name: 'block', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
],
responses: [new OA\Response(response: 200, description: 'Blok silindi')],
)]
public function destroy(Course $course, CourseBlock $block): JsonResponse
{
$block->delete();
return response()->json(['message' => 'Blok silindi.']);
}
#[OA\Post(
path: '/api/admin/courses/{course}/blocks/reorder',
summary: 'Eğitim blok sıralamasını güncelle',
tags: ['Admin - Course Blocks'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'course', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
required: ['items'],
properties: [
new OA\Property(property: 'items', type: 'array', items: new OA\Items(
properties: [
new OA\Property(property: 'id', type: 'integer'),
new OA\Property(property: 'order_index', type: 'integer'),
],
)),
],
)),
responses: [new OA\Response(response: 200, description: 'Sıralama güncellendi')],
)]
public function reorder(Request $request, Course $course): JsonResponse
{
$validated = $request->validate([
'items' => ['required', 'array', 'min:1'],
'items.*.id' => ['required', 'integer', 'exists:course_blocks,id'],
'items.*.order_index' => ['required', 'integer', 'min:0'],
]);
foreach ($validated['items'] as $item) {
$course->blocks()
->where('id', $item['id'])
->update(['order_index' => $item['order_index']]);
}
return response()->json(['message' => 'Blok sıralaması güncellendi.']);
}
}

View File

@@ -0,0 +1,163 @@
<?php
namespace App\Http\Controllers\Api\Admin;
use App\Actions\Course\CreateCourseAction;
use App\Actions\Course\DeleteCourseAction;
use App\Actions\Course\UpdateCourseAction;
use App\DTOs\CourseData;
use App\Http\Controllers\Controller;
use App\Http\Requests\Course\StoreCourseRequest;
use App\Http\Requests\Course\UpdateCourseRequest;
use App\Http\Resources\CourseResource;
use App\Models\Course;
use App\Repositories\Contracts\CourseRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use OpenApi\Attributes as OA;
class CourseController extends Controller
{
public function __construct(private CourseRepositoryInterface $repository) {}
#[OA\Get(
path: '/api/admin/courses',
summary: 'Eğitimleri listele (Admin)',
tags: ['Admin - Courses'],
security: [['sanctum' => []]],
parameters: [
new OA\Parameter(name: 'category', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'search', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'sort', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'per_page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
],
responses: [new OA\Response(response: 200, description: 'Eğitim listesi')],
)]
public function index(Request $request): AnonymousResourceCollection
{
$courses = $this->repository->paginate(
filters: $request->only('category', 'search', 'sort'),
perPage: $request->integer('per_page', 15),
);
return CourseResource::collection($courses);
}
#[OA\Post(
path: '/api/admin/courses',
summary: 'Yeni eğitim oluştur',
tags: ['Admin - Courses'],
security: [['sanctum' => []]],
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
required: ['category_id', 'title', 'slug', 'desc', 'long_desc', 'duration'],
properties: [
new OA\Property(property: 'category_id', type: 'integer', description: 'Kategori ID'),
new OA\Property(property: 'slug', type: 'string', description: 'URL slug (unique)'),
new OA\Property(property: 'title', type: 'string', description: 'Eğitim başlığı'),
new OA\Property(property: 'sub', type: 'string', nullable: true, description: 'Alt başlık. Örn: STCW II/1'),
new OA\Property(property: 'desc', type: 'string', description: 'Kısa açıklama'),
new OA\Property(property: 'long_desc', type: 'string', description: 'Detaylııklama'),
new OA\Property(property: 'duration', type: 'string', description: 'Süre. Örn: 5 Gün'),
new OA\Property(property: 'students', type: 'integer', description: 'Öğrenci sayısı'),
new OA\Property(property: 'rating', type: 'number', format: 'float', description: 'Puan (0-5)'),
new OA\Property(property: 'badge', type: 'string', nullable: true, description: 'Rozet. Örn: Simülatör'),
new OA\Property(property: 'image', type: 'string', nullable: true, description: 'Görsel path'),
new OA\Property(property: 'price', type: 'string', nullable: true, description: 'Fiyat. Örn: 5.000 TL'),
new OA\Property(property: 'includes', type: 'array', items: new OA\Items(type: 'string'), nullable: true, description: 'Fiyata dahil olanlar'),
new OA\Property(property: 'requirements', type: 'array', items: new OA\Items(type: 'string'), nullable: true, description: 'Katılım koşulları'),
new OA\Property(property: 'meta_title', type: 'string', nullable: true, description: 'SEO Title'),
new OA\Property(property: 'meta_description', type: 'string', nullable: true, description: 'SEO Description'),
new OA\Property(property: 'scope', type: 'array', items: new OA\Items(type: 'string'), nullable: true, description: 'Eğitim kapsamı konu başlıkları'),
new OA\Property(property: 'standard', type: 'string', nullable: true, description: 'Uyum standardı. Örn: STCW / IMO Uyumlu'),
new OA\Property(property: 'language', type: 'string', nullable: true, description: 'Eğitim dili. Varsayılan: Türkçe'),
new OA\Property(property: 'location', type: 'string', nullable: true, description: 'Varsayılan lokasyon. Örn: Kadıköy, İstanbul'),
],
)),
responses: [
new OA\Response(response: 201, description: 'Eğitim oluşturuldu'),
new OA\Response(response: 422, description: 'Validasyon hatası'),
],
)]
public function store(StoreCourseRequest $request, CreateCourseAction $action): JsonResponse
{
$dto = CourseData::fromArray($request->validated());
$course = $action->execute($dto);
return (new CourseResource($course->load('category')))
->response()
->setStatusCode(201);
}
#[OA\Get(
path: '/api/admin/courses/{course}',
summary: 'Eğitim detayı (Admin)',
tags: ['Admin - Courses'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'course', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
responses: [new OA\Response(response: 200, description: 'Eğitim detayı')],
)]
public function show(Course $course): CourseResource
{
return new CourseResource($course->load(['category', 'schedules', 'blocks']));
}
#[OA\Put(
path: '/api/admin/courses/{course}',
summary: 'Eğitim güncelle',
tags: ['Admin - Courses'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'course', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
required: ['category_id', 'title', 'slug', 'desc', 'long_desc', 'duration'],
properties: [
new OA\Property(property: 'category_id', type: 'integer', description: 'Kategori ID'),
new OA\Property(property: 'slug', type: 'string', description: 'URL slug (unique)'),
new OA\Property(property: 'title', type: 'string', description: 'Eğitim başlığı'),
new OA\Property(property: 'sub', type: 'string', nullable: true, description: 'Alt başlık'),
new OA\Property(property: 'desc', type: 'string', description: 'Kısa açıklama'),
new OA\Property(property: 'long_desc', type: 'string', description: 'Detaylııklama'),
new OA\Property(property: 'duration', type: 'string', description: 'Süre. Örn: 5 Gün'),
new OA\Property(property: 'students', type: 'integer', description: 'Öğrenci sayısı'),
new OA\Property(property: 'rating', type: 'number', format: 'float', description: 'Puan (0-5)'),
new OA\Property(property: 'badge', type: 'string', nullable: true, description: 'Rozet'),
new OA\Property(property: 'image', type: 'string', nullable: true, description: 'Görsel path'),
new OA\Property(property: 'price', type: 'string', nullable: true, description: 'Fiyat'),
new OA\Property(property: 'includes', type: 'array', items: new OA\Items(type: 'string'), nullable: true, description: 'Fiyata dahil olanlar'),
new OA\Property(property: 'requirements', type: 'array', items: new OA\Items(type: 'string'), nullable: true, description: 'Katılım koşulları'),
new OA\Property(property: 'meta_title', type: 'string', nullable: true, description: 'SEO Title'),
new OA\Property(property: 'meta_description', type: 'string', nullable: true, description: 'SEO Description'),
new OA\Property(property: 'scope', type: 'array', items: new OA\Items(type: 'string'), nullable: true, description: 'Eğitim kapsamı konu başlıkları'),
new OA\Property(property: 'standard', type: 'string', nullable: true, description: 'Uyum standardı'),
new OA\Property(property: 'language', type: 'string', nullable: true, description: 'Eğitim dili'),
new OA\Property(property: 'location', type: 'string', nullable: true, description: 'Varsayılan lokasyon'),
],
)),
responses: [
new OA\Response(response: 200, description: 'Eğitim güncellendi'),
new OA\Response(response: 422, description: 'Validasyon hatası'),
],
)]
public function update(UpdateCourseRequest $request, Course $course, UpdateCourseAction $action): CourseResource
{
$dto = CourseData::fromArray($request->validated());
$course = $action->execute($course, $dto);
return new CourseResource($course->load('category'));
}
#[OA\Delete(
path: '/api/admin/courses/{course}',
summary: 'Eğitim sil',
tags: ['Admin - Courses'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'course', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
responses: [new OA\Response(response: 200, description: 'Eğitim silindi')],
)]
public function destroy(Course $course, DeleteCourseAction $action): JsonResponse
{
$action->execute($course);
return response()->json(['message' => 'Eğitim başarıyla silindi.']);
}
}

View File

@@ -0,0 +1,128 @@
<?php
namespace App\Http\Controllers\Api\Admin;
use App\Actions\Faq\CreateFaqAction;
use App\Actions\Faq\DeleteFaqAction;
use App\Actions\Faq\UpdateFaqAction;
use App\DTOs\FaqData;
use App\Http\Controllers\Controller;
use App\Http\Requests\Faq\StoreFaqRequest;
use App\Http\Requests\Faq\UpdateFaqRequest;
use App\Http\Resources\FaqResource;
use App\Models\Faq;
use App\Repositories\Contracts\FaqRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use OpenApi\Attributes as OA;
class FaqController extends Controller
{
public function __construct(private FaqRepositoryInterface $repository) {}
#[OA\Get(
path: '/api/admin/faqs',
summary: 'SSS listele (Admin)',
tags: ['Admin - FAQs'],
security: [['sanctum' => []]],
parameters: [
new OA\Parameter(name: 'category', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'per_page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 50)),
],
responses: [new OA\Response(response: 200, description: 'SSS listesi')],
)]
public function index(Request $request): AnonymousResourceCollection
{
$faqs = $this->repository->paginate(
$request->only(['category']),
$request->integer('per_page', 50),
);
return FaqResource::collection($faqs);
}
#[OA\Post(
path: '/api/admin/faqs',
summary: 'Yeni SSS oluştur',
tags: ['Admin - FAQs'],
security: [['sanctum' => []]],
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
required: ['question', 'answer', 'category'],
properties: [
new OA\Property(property: 'question', type: 'string'),
new OA\Property(property: 'answer', type: 'string'),
new OA\Property(property: 'category', type: 'string'),
new OA\Property(property: 'is_active', type: 'boolean'),
new OA\Property(property: 'sort_order', type: 'integer'),
],
)),
responses: [
new OA\Response(response: 201, description: 'SSS oluşturuldu'),
new OA\Response(response: 422, description: 'Validasyon hatası'),
],
)]
public function store(StoreFaqRequest $request, CreateFaqAction $action): JsonResponse
{
$dto = FaqData::fromArray($request->validated());
$faq = $action->execute($dto);
return response()->json(new FaqResource($faq), 201);
}
#[OA\Get(
path: '/api/admin/faqs/{faq}',
summary: 'SSS detayı',
tags: ['Admin - FAQs'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'faq', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
responses: [new OA\Response(response: 200, description: 'SSS detayı')],
)]
public function show(Faq $faq): JsonResponse
{
return response()->json(new FaqResource($faq));
}
#[OA\Put(
path: '/api/admin/faqs/{faq}',
summary: 'SSS güncelle',
tags: ['Admin - FAQs'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'faq', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
properties: [
new OA\Property(property: 'question', type: 'string'),
new OA\Property(property: 'answer', type: 'string'),
new OA\Property(property: 'category', type: 'string'),
new OA\Property(property: 'is_active', type: 'boolean'),
new OA\Property(property: 'sort_order', type: 'integer'),
],
)),
responses: [
new OA\Response(response: 200, description: 'SSS güncellendi'),
new OA\Response(response: 422, description: 'Validasyon hatası'),
],
)]
public function update(UpdateFaqRequest $request, Faq $faq, UpdateFaqAction $action): JsonResponse
{
$dto = FaqData::fromArray(array_merge($faq->toArray(), $request->validated()));
$faq = $action->execute($faq, $dto);
return response()->json(new FaqResource($faq));
}
#[OA\Delete(
path: '/api/admin/faqs/{faq}',
summary: 'SSS sil',
tags: ['Admin - FAQs'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'faq', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
responses: [new OA\Response(response: 200, description: 'SSS silindi')],
)]
public function destroy(Faq $faq, DeleteFaqAction $action): JsonResponse
{
$action->execute($faq);
return response()->json(['message' => 'SSS silindi.']);
}
}

View File

@@ -0,0 +1,127 @@
<?php
namespace App\Http\Controllers\Api\Admin;
use App\Actions\GuideCard\CreateGuideCardAction;
use App\Actions\GuideCard\DeleteGuideCardAction;
use App\Actions\GuideCard\UpdateGuideCardAction;
use App\DTOs\GuideCardData;
use App\Http\Controllers\Controller;
use App\Http\Requests\GuideCard\StoreGuideCardRequest;
use App\Http\Requests\GuideCard\UpdateGuideCardRequest;
use App\Http\Resources\GuideCardResource;
use App\Models\GuideCard;
use App\Repositories\Contracts\GuideCardRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use OpenApi\Attributes as OA;
class GuideCardController extends Controller
{
public function __construct(private GuideCardRepositoryInterface $repository) {}
#[OA\Get(
path: '/api/admin/guide-cards',
summary: 'Rehber kartları listele (Admin)',
tags: ['Admin - Guide Cards'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'per_page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15))],
responses: [new OA\Response(response: 200, description: 'Kart listesi')],
)]
public function index(Request $request): AnonymousResourceCollection
{
$cards = $this->repository->paginate(
$request->only([]),
$request->integer('per_page', 15),
);
return GuideCardResource::collection($cards);
}
#[OA\Post(
path: '/api/admin/guide-cards',
summary: 'Yeni rehber kartı oluştur',
tags: ['Admin - Guide Cards'],
security: [['sanctum' => []]],
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
required: ['title', 'description', 'icon'],
properties: [
new OA\Property(property: 'title', type: 'string'),
new OA\Property(property: 'description', type: 'string'),
new OA\Property(property: 'icon', type: 'string'),
new OA\Property(property: 'url', type: 'string'),
new OA\Property(property: 'is_active', type: 'boolean'),
new OA\Property(property: 'sort_order', type: 'integer'),
],
)),
responses: [
new OA\Response(response: 201, description: 'Kart oluşturuldu'),
new OA\Response(response: 422, description: 'Validasyon hatası'),
],
)]
public function store(StoreGuideCardRequest $request, CreateGuideCardAction $action): JsonResponse
{
$dto = GuideCardData::fromArray($request->validated());
$card = $action->execute($dto);
return response()->json(new GuideCardResource($card), 201);
}
#[OA\Get(
path: '/api/admin/guide-cards/{guideCard}',
summary: 'Rehber kart detayı',
tags: ['Admin - Guide Cards'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'guideCard', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
responses: [new OA\Response(response: 200, description: 'Kart detayı')],
)]
public function show(GuideCard $guideCard): JsonResponse
{
return response()->json(new GuideCardResource($guideCard));
}
#[OA\Put(
path: '/api/admin/guide-cards/{guideCard}',
summary: 'Rehber kart güncelle',
tags: ['Admin - Guide Cards'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'guideCard', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
properties: [
new OA\Property(property: 'title', type: 'string'),
new OA\Property(property: 'description', type: 'string'),
new OA\Property(property: 'icon', type: 'string'),
new OA\Property(property: 'url', type: 'string'),
new OA\Property(property: 'is_active', type: 'boolean'),
new OA\Property(property: 'sort_order', type: 'integer'),
],
)),
responses: [
new OA\Response(response: 200, description: 'Kart güncellendi'),
new OA\Response(response: 422, description: 'Validasyon hatası'),
],
)]
public function update(UpdateGuideCardRequest $request, GuideCard $guideCard, UpdateGuideCardAction $action): JsonResponse
{
$dto = GuideCardData::fromArray(array_merge($guideCard->toArray(), $request->validated()));
$guideCard = $action->execute($guideCard, $dto);
return response()->json(new GuideCardResource($guideCard));
}
#[OA\Delete(
path: '/api/admin/guide-cards/{guideCard}',
summary: 'Rehber kart sil',
tags: ['Admin - Guide Cards'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'guideCard', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
responses: [new OA\Response(response: 200, description: 'Kart silindi')],
)]
public function destroy(GuideCard $guideCard, DeleteGuideCardAction $action): JsonResponse
{
$action->execute($guideCard);
return response()->json(['message' => 'Rehber kartı silindi.']);
}
}

View File

@@ -0,0 +1,130 @@
<?php
namespace App\Http\Controllers\Api\Admin;
use App\Actions\HeroSlide\CreateHeroSlideAction;
use App\Actions\HeroSlide\DeleteHeroSlideAction;
use App\Actions\HeroSlide\UpdateHeroSlideAction;
use App\DTOs\HeroSlideData;
use App\Http\Controllers\Controller;
use App\Http\Requests\HeroSlide\StoreHeroSlideRequest;
use App\Http\Requests\HeroSlide\UpdateHeroSlideRequest;
use App\Http\Resources\HeroSlideResource;
use App\Models\HeroSlide;
use App\Repositories\Contracts\HeroSlideRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use OpenApi\Attributes as OA;
class HeroSlideController extends Controller
{
public function __construct(private HeroSlideRepositoryInterface $repository) {}
#[OA\Get(
path: '/api/admin/hero-slides',
summary: 'Hero slide listele (Admin)',
tags: ['Admin - Hero Slides'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'per_page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15))],
responses: [new OA\Response(response: 200, description: 'Slide listesi')],
)]
public function index(Request $request): AnonymousResourceCollection
{
$slides = $this->repository->paginate(
$request->only([]),
$request->integer('per_page', 15),
);
return HeroSlideResource::collection($slides);
}
#[OA\Post(
path: '/api/admin/hero-slides',
summary: 'Yeni hero slide oluştur',
tags: ['Admin - Hero Slides'],
security: [['sanctum' => []]],
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
required: ['title', 'image'],
properties: [
new OA\Property(property: 'title', type: 'string'),
new OA\Property(property: 'subtitle', type: 'string'),
new OA\Property(property: 'image', type: 'string'),
new OA\Property(property: 'mobile_image', type: 'string'),
new OA\Property(property: 'button_text', type: 'string'),
new OA\Property(property: 'button_url', type: 'string'),
new OA\Property(property: 'is_active', type: 'boolean'),
new OA\Property(property: 'sort_order', type: 'integer'),
],
)),
responses: [
new OA\Response(response: 201, description: 'Slide oluşturuldu'),
new OA\Response(response: 422, description: 'Validasyon hatası'),
],
)]
public function store(StoreHeroSlideRequest $request, CreateHeroSlideAction $action): JsonResponse
{
$dto = HeroSlideData::fromArray($request->validated());
$slide = $action->execute($dto);
return response()->json(new HeroSlideResource($slide), 201);
}
#[OA\Get(
path: '/api/admin/hero-slides/{heroSlide}',
summary: 'Hero slide detayı',
tags: ['Admin - Hero Slides'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'heroSlide', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
responses: [new OA\Response(response: 200, description: 'Slide detayı')],
)]
public function show(HeroSlide $heroSlide): JsonResponse
{
return response()->json(new HeroSlideResource($heroSlide));
}
#[OA\Put(
path: '/api/admin/hero-slides/{heroSlide}',
summary: 'Hero slide güncelle',
tags: ['Admin - Hero Slides'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'heroSlide', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
properties: [
new OA\Property(property: 'title', type: 'string'),
new OA\Property(property: 'subtitle', type: 'string'),
new OA\Property(property: 'image', type: 'string'),
new OA\Property(property: 'button_text', type: 'string'),
new OA\Property(property: 'button_url', type: 'string'),
new OA\Property(property: 'is_active', type: 'boolean'),
new OA\Property(property: 'sort_order', type: 'integer'),
],
)),
responses: [
new OA\Response(response: 200, description: 'Slide güncellendi'),
new OA\Response(response: 422, description: 'Validasyon hatası'),
],
)]
public function update(UpdateHeroSlideRequest $request, HeroSlide $heroSlide, UpdateHeroSlideAction $action): JsonResponse
{
$dto = HeroSlideData::fromArray(array_merge($heroSlide->toArray(), $request->validated()));
$heroSlide = $action->execute($heroSlide, $dto);
return response()->json(new HeroSlideResource($heroSlide));
}
#[OA\Delete(
path: '/api/admin/hero-slides/{heroSlide}',
summary: 'Hero slide sil',
tags: ['Admin - Hero Slides'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'heroSlide', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
responses: [new OA\Response(response: 200, description: 'Slide silindi')],
)]
public function destroy(HeroSlide $heroSlide, DeleteHeroSlideAction $action): JsonResponse
{
$action->execute($heroSlide);
return response()->json(['message' => 'Hero slide silindi.']);
}
}

View File

@@ -0,0 +1,103 @@
<?php
namespace App\Http\Controllers\Api\Admin;
use App\Actions\Lead\DeleteLeadAction;
use App\Actions\Lead\UpdateLeadAction;
use App\DTOs\LeadData;
use App\Http\Controllers\Controller;
use App\Http\Requests\Lead\UpdateLeadRequest;
use App\Http\Resources\LeadResource;
use App\Models\Lead;
use App\Repositories\Contracts\LeadRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use OpenApi\Attributes as OA;
class LeadController extends Controller
{
public function __construct(private LeadRepositoryInterface $repository) {}
#[OA\Get(
path: '/api/admin/leads',
summary: 'Başvuruları listele (Admin)',
tags: ['Admin - Leads'],
security: [['sanctum' => []]],
parameters: [
new OA\Parameter(name: 'status', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'source', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'is_read', in: 'query', required: false, schema: new OA\Schema(type: 'boolean')),
new OA\Parameter(name: 'search', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'per_page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
],
responses: [new OA\Response(response: 200, description: 'Başvuru listesi')],
)]
public function index(Request $request): AnonymousResourceCollection
{
$leads = $this->repository->paginate(
$request->only(['status', 'source', 'is_read', 'search']),
$request->integer('per_page', 15),
);
return LeadResource::collection($leads);
}
#[OA\Get(
path: '/api/admin/leads/{lead}',
summary: 'Başvuru detayı',
tags: ['Admin - Leads'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'lead', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
responses: [new OA\Response(response: 200, description: 'Başvuru detayı')],
)]
public function show(Lead $lead): JsonResponse
{
if (! $lead->is_read) {
$lead->update(['is_read' => true]);
}
return response()->json(new LeadResource($lead));
}
#[OA\Put(
path: '/api/admin/leads/{lead}',
summary: 'Başvuru güncelle',
tags: ['Admin - Leads'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'lead', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
properties: [
new OA\Property(property: 'status', type: 'string'),
new OA\Property(property: 'is_read', type: 'boolean'),
new OA\Property(property: 'admin_notes', type: 'string'),
],
)),
responses: [
new OA\Response(response: 200, description: 'Başvuru güncellendi'),
new OA\Response(response: 422, description: 'Validasyon hatası'),
],
)]
public function update(UpdateLeadRequest $request, Lead $lead, UpdateLeadAction $action): JsonResponse
{
$dto = LeadData::fromArray(array_merge($lead->toArray(), $request->validated()));
$lead = $action->execute($lead, $dto);
return response()->json(new LeadResource($lead));
}
#[OA\Delete(
path: '/api/admin/leads/{lead}',
summary: 'Başvuru sil',
tags: ['Admin - Leads'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'lead', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
responses: [new OA\Response(response: 200, description: 'Başvuru silindi')],
)]
public function destroy(Lead $lead, DeleteLeadAction $action): JsonResponse
{
$action->execute($lead);
return response()->json(['message' => 'Talep silindi.']);
}
}

View File

@@ -0,0 +1,159 @@
<?php
namespace App\Http\Controllers\Api\Admin;
use App\Actions\Menu\CreateMenuAction;
use App\Actions\Menu\DeleteMenuAction;
use App\Actions\Menu\UpdateMenuAction;
use App\DTOs\MenuData;
use App\Http\Controllers\Controller;
use App\Http\Requests\Menu\ReorderMenuRequest;
use App\Http\Requests\Menu\StoreMenuRequest;
use App\Http\Requests\Menu\UpdateMenuRequest;
use App\Http\Resources\MenuResource;
use App\Models\Menu;
use App\Repositories\Contracts\MenuRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use OpenApi\Attributes as OA;
class MenuController extends Controller
{
public function __construct(private MenuRepositoryInterface $repository) {}
#[OA\Get(
path: '/api/admin/menus',
summary: 'Menü öğelerini listele (Admin)',
tags: ['Admin - Menus'],
security: [['sanctum' => []]],
parameters: [
new OA\Parameter(name: 'location', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'per_page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 50)),
],
responses: [new OA\Response(response: 200, description: 'Menü listesi')],
)]
public function index(Request $request): AnonymousResourceCollection
{
$menus = $this->repository->paginate(
$request->only(['location']),
$request->integer('per_page', 50),
);
return MenuResource::collection($menus);
}
#[OA\Post(
path: '/api/admin/menus',
summary: 'Yeni menü öğesi oluştur',
tags: ['Admin - Menus'],
security: [['sanctum' => []]],
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
required: ['title', 'url', 'location'],
properties: [
new OA\Property(property: 'title', type: 'string'),
new OA\Property(property: 'url', type: 'string'),
new OA\Property(property: 'location', type: 'string'),
new OA\Property(property: 'parent_id', type: 'integer'),
new OA\Property(property: 'target', type: 'string'),
new OA\Property(property: 'icon', type: 'string'),
new OA\Property(property: 'is_active', type: 'boolean'),
new OA\Property(property: 'sort_order', type: 'integer'),
],
)),
responses: [
new OA\Response(response: 201, description: 'Menü oluşturuldu'),
new OA\Response(response: 422, description: 'Validasyon hatası'),
],
)]
public function store(StoreMenuRequest $request, CreateMenuAction $action): JsonResponse
{
$dto = MenuData::fromArray($request->validated());
$menu = $action->execute($dto);
return response()->json(new MenuResource($menu), 201);
}
#[OA\Get(
path: '/api/admin/menus/{menu}',
summary: 'Menü detayı',
tags: ['Admin - Menus'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'menu', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
responses: [new OA\Response(response: 200, description: 'Menü detayı')],
)]
public function show(Menu $menu): JsonResponse
{
return response()->json(new MenuResource($menu->load('children')));
}
#[OA\Put(
path: '/api/admin/menus/{menu}',
summary: 'Menü güncelle',
tags: ['Admin - Menus'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'menu', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
properties: [
new OA\Property(property: 'title', type: 'string'),
new OA\Property(property: 'url', type: 'string'),
new OA\Property(property: 'location', type: 'string'),
new OA\Property(property: 'parent_id', type: 'integer'),
new OA\Property(property: 'is_active', type: 'boolean'),
new OA\Property(property: 'sort_order', type: 'integer'),
],
)),
responses: [
new OA\Response(response: 200, description: 'Menü güncellendi'),
new OA\Response(response: 422, description: 'Validasyon hatası'),
],
)]
public function update(UpdateMenuRequest $request, Menu $menu, UpdateMenuAction $action): JsonResponse
{
$dto = MenuData::fromArray(array_merge($menu->toArray(), $request->validated()));
$menu = $action->execute($menu, $dto);
return response()->json(new MenuResource($menu->load('children')));
}
#[OA\Delete(
path: '/api/admin/menus/{menu}',
summary: 'Menü sil',
tags: ['Admin - Menus'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'menu', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
responses: [new OA\Response(response: 200, description: 'Menü silindi')],
)]
public function destroy(Menu $menu, DeleteMenuAction $action): JsonResponse
{
$action->execute($menu);
return response()->json(['message' => 'Menü silindi.']);
}
#[OA\Post(
path: '/api/admin/menus/reorder',
summary: 'Menü sıralamasını güncelle',
tags: ['Admin - Menus'],
security: [['sanctum' => []]],
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
required: ['items'],
properties: [
new OA\Property(property: 'items', type: 'array', items: new OA\Items(
properties: [
new OA\Property(property: 'id', type: 'integer'),
new OA\Property(property: 'order_index', type: 'integer'),
new OA\Property(property: 'parent_id', type: 'integer', nullable: true),
],
)),
],
)),
responses: [new OA\Response(response: 200, description: 'Sıralama güncellendi')],
)]
public function reorder(ReorderMenuRequest $request): JsonResponse
{
$this->repository->reorder($request->validated('items'));
return response()->json(['message' => 'Menü sıralaması güncellendi.']);
}
}

View File

@@ -0,0 +1,179 @@
<?php
namespace App\Http\Controllers\Api\Admin;
use App\Actions\Page\CreatePageAction;
use App\Actions\Page\DeletePageAction;
use App\Actions\Page\UpdatePageAction;
use App\DTOs\PageData;
use App\Http\Controllers\Controller;
use App\Http\Requests\Page\StorePageRequest;
use App\Http\Requests\Page\UpdatePageRequest;
use App\Http\Resources\PageResource;
use App\Models\Page;
use App\Repositories\Contracts\PageRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Support\Facades\DB;
use OpenApi\Attributes as OA;
class PageController extends Controller
{
public function __construct(private PageRepositoryInterface $repository) {}
#[OA\Get(
path: '/api/admin/pages',
summary: 'Sayfaları listele (Admin)',
tags: ['Admin - Pages'],
security: [['sanctum' => []]],
parameters: [
new OA\Parameter(name: 'search', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'per_page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
],
responses: [new OA\Response(response: 200, description: 'Sayfa listesi')],
)]
public function index(Request $request): AnonymousResourceCollection
{
$pages = $this->repository->paginate(
$request->only(['search']),
$request->integer('per_page', 15),
);
return PageResource::collection($pages);
}
#[OA\Post(
path: '/api/admin/pages',
summary: 'Yeni sayfa oluştur',
tags: ['Admin - Pages'],
security: [['sanctum' => []]],
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
required: ['title', 'slug'],
properties: [
new OA\Property(property: 'title', type: 'string'),
new OA\Property(property: 'slug', type: 'string'),
new OA\Property(property: 'content', type: 'string'),
new OA\Property(property: 'template', type: 'string'),
new OA\Property(property: 'is_active', type: 'boolean'),
new OA\Property(property: 'meta_title', type: 'string'),
new OA\Property(property: 'meta_description', type: 'string'),
new OA\Property(property: 'blocks', type: 'array', items: new OA\Items(
properties: [
new OA\Property(property: 'type', type: 'string'),
new OA\Property(property: 'content', type: 'object'),
new OA\Property(property: 'order_index', type: 'integer'),
],
)),
],
)),
responses: [
new OA\Response(response: 201, description: 'Sayfa oluşturuldu'),
new OA\Response(response: 422, description: 'Validasyon hatası'),
],
)]
public function store(StorePageRequest $request, CreatePageAction $action): JsonResponse
{
return DB::transaction(function () use ($request, $action) {
$validated = $request->validated();
$blocks = $validated['blocks'] ?? [];
unset($validated['blocks']);
$dto = PageData::fromArray($validated);
$page = $action->execute($dto);
foreach ($blocks as $index => $block) {
$page->blocks()->create([
'type' => $block['type'],
'content' => $block['content'],
'order_index' => $block['order_index'] ?? $index,
]);
}
return response()->json(new PageResource($page->load('blocks')), 201);
});
}
#[OA\Get(
path: '/api/admin/pages/{page}',
summary: 'Sayfa detayı (Admin)',
tags: ['Admin - Pages'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'page', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
responses: [new OA\Response(response: 200, description: 'Sayfa detayı')],
)]
public function show(Page $page): JsonResponse
{
return response()->json(new PageResource($page->load('blocks')));
}
#[OA\Put(
path: '/api/admin/pages/{page}',
summary: 'Sayfa güncelle',
tags: ['Admin - Pages'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'page', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
properties: [
new OA\Property(property: 'title', type: 'string'),
new OA\Property(property: 'slug', type: 'string'),
new OA\Property(property: 'content', type: 'string'),
new OA\Property(property: 'template', type: 'string'),
new OA\Property(property: 'is_active', type: 'boolean'),
new OA\Property(property: 'meta_title', type: 'string'),
new OA\Property(property: 'meta_description', type: 'string'),
new OA\Property(property: 'blocks', type: 'array', items: new OA\Items(
properties: [
new OA\Property(property: 'type', type: 'string'),
new OA\Property(property: 'content', type: 'object'),
new OA\Property(property: 'order_index', type: 'integer'),
],
)),
],
)),
responses: [
new OA\Response(response: 200, description: 'Sayfa güncellendi'),
new OA\Response(response: 422, description: 'Validasyon hatası'),
],
)]
public function update(UpdatePageRequest $request, Page $page, UpdatePageAction $action): JsonResponse
{
return DB::transaction(function () use ($request, $page, $action) {
$validated = $request->validated();
$blocks = $validated['blocks'] ?? null;
unset($validated['blocks']);
$dto = PageData::fromArray(array_merge($page->toArray(), $validated));
$page = $action->execute($page, $dto);
if ($blocks !== null) {
$page->blocks()->delete();
foreach ($blocks as $index => $block) {
$page->blocks()->create([
'type' => $block['type'],
'content' => $block['content'],
'order_index' => $block['order_index'] ?? $index,
]);
}
}
return response()->json(new PageResource($page->load('blocks')));
});
}
#[OA\Delete(
path: '/api/admin/pages/{page}',
summary: 'Sayfa sil',
tags: ['Admin - Pages'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'page', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
responses: [new OA\Response(response: 200, description: 'Sayfa silindi')],
)]
public function destroy(Page $page, DeletePageAction $action): JsonResponse
{
$page->blocks()->delete();
$action->execute($page);
return response()->json(['message' => 'Sayfa silindi.']);
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace App\Http\Controllers\Api\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\Preview\StorePreviewRequest;
use App\Models\Page;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use OpenApi\Attributes as OA;
class PreviewController extends Controller
{
private const CACHE_TTL = 600; // 10 minutes
#[OA\Post(
path: '/api/admin/preview',
summary: 'Önizleme oluştur',
tags: ['Admin - Preview'],
security: [['sanctum' => []]],
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
required: ['page_id', 'blocks'],
properties: [
new OA\Property(property: 'page_id', type: 'integer'),
new OA\Property(property: 'blocks', type: 'array', items: new OA\Items(
properties: [
new OA\Property(property: 'type', type: 'string'),
new OA\Property(property: 'content', type: 'object'),
new OA\Property(property: 'order_index', type: 'integer'),
],
)),
],
)),
responses: [
new OA\Response(response: 201, description: 'Önizleme oluşturuldu'),
new OA\Response(response: 422, description: 'Validasyon hatası'),
],
)]
public function store(StorePreviewRequest $request): JsonResponse
{
$validated = $request->validated();
$page = Page::findOrFail($validated['page_id']);
$token = (string) Str::uuid();
$blocks = collect($validated['blocks'])
->sortBy('order_index')
->values()
->map(fn (array $block, int $index) => [
'id' => $index + 1,
'type' => $block['type'],
'content' => $block['content'],
'order_index' => $block['order_index'],
])
->all();
Cache::put("preview_{$token}", [
'id' => $page->id,
'slug' => $page->slug,
'title' => $page->title,
'meta_title' => $page->meta_title,
'meta_description' => $page->meta_description,
'is_active' => $page->is_active,
'blocks' => $blocks,
'created_at' => $page->created_at?->toISOString(),
'updated_at' => now()->toISOString(),
], self::CACHE_TTL);
$previewUrl = config('app.frontend_url')."/api/preview?token={$token}&slug={$page->slug}";
return response()->json([
'token' => $token,
'preview_url' => $previewUrl,
'expires_in' => self::CACHE_TTL,
], 201);
}
#[OA\Delete(
path: '/api/admin/preview/{token}',
summary: 'Önizlemeyi sil',
tags: ['Admin - Preview'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'token', in: 'path', required: true, schema: new OA\Schema(type: 'string'))],
responses: [new OA\Response(response: 200, description: 'Önizleme silindi')],
)]
public function destroy(string $token): JsonResponse
{
Cache::forget("preview_{$token}");
return response()->json(['message' => 'Önizleme silindi.']);
}
}

View File

@@ -0,0 +1,180 @@
<?php
namespace App\Http\Controllers\Api\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\Role\StoreRoleRequest;
use App\Http\Requests\Role\UpdateRoleRequest;
use App\Http\Resources\RoleResource;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use OpenApi\Attributes as OA;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
class RoleController extends Controller
{
#[OA\Get(
path: '/api/admin/roles',
summary: 'Rolleri listele',
tags: ['Admin - Roles'],
security: [['sanctum' => []]],
responses: [new OA\Response(response: 200, description: 'Rol listesi')],
)]
public function index(): AnonymousResourceCollection
{
$roles = Role::query()
->with('permissions')
->get()
->loadCount('users');
return RoleResource::collection($roles);
}
#[OA\Post(
path: '/api/admin/roles',
summary: 'Yeni rol oluştur',
tags: ['Admin - Roles'],
security: [['sanctum' => []]],
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(
required: ['name', 'permissions'],
properties: [
new OA\Property(property: 'name', type: 'string', example: 'moderator'),
new OA\Property(property: 'permissions', type: 'array', items: new OA\Items(type: 'string'), example: ['view-category', 'view-course']),
],
),
),
responses: [
new OA\Response(response: 201, description: 'Rol oluşturuldu'),
new OA\Response(response: 422, description: 'Validasyon hatası'),
],
)]
public function store(StoreRoleRequest $request): JsonResponse
{
$role = Role::create([
'name' => $request->validated('name'),
'guard_name' => 'web',
]);
$role->syncPermissions($request->validated('permissions'));
$role->load('permissions');
return (new RoleResource($role))
->response()
->setStatusCode(201);
}
#[OA\Get(
path: '/api/admin/roles/{role}',
summary: 'Rol detayı',
tags: ['Admin - Roles'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'role', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
responses: [
new OA\Response(response: 200, description: 'Rol detayı'),
new OA\Response(response: 404, description: 'Bulunamadı'),
],
)]
public function show(Role $role): RoleResource
{
$role->load('permissions');
$role->loadCount('users');
return new RoleResource($role);
}
#[OA\Put(
path: '/api/admin/roles/{role}',
summary: 'Rol güncelle',
tags: ['Admin - Roles'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'role', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
properties: [
new OA\Property(property: 'name', type: 'string', example: 'moderator'),
new OA\Property(property: 'permissions', type: 'array', items: new OA\Items(type: 'string')),
],
)),
responses: [
new OA\Response(response: 200, description: 'Rol güncellendi'),
new OA\Response(response: 422, description: 'Validasyon hatası'),
],
)]
public function update(UpdateRoleRequest $request, Role $role): RoleResource
{
$validated = $request->validated();
if (isset($validated['name'])) {
$role->update(['name' => $validated['name']]);
}
if (isset($validated['permissions'])) {
$role->syncPermissions($validated['permissions']);
}
$role->load('permissions');
return new RoleResource($role);
}
#[OA\Delete(
path: '/api/admin/roles/{role}',
summary: 'Rol sil',
tags: ['Admin - Roles'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'role', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
responses: [
new OA\Response(response: 200, description: 'Rol silindi'),
new OA\Response(response: 403, description: 'Varsayılan roller silinemez'),
new OA\Response(response: 404, description: 'Bulunamadı'),
],
)]
public function destroy(Role $role): JsonResponse
{
if (in_array($role->name, ['super-admin', 'editor'])) {
return response()->json(['message' => 'Varsayılan roller silinemez.'], 403);
}
if ($role->users()->count() > 0) {
return response()->json(['message' => 'Bu role atanmış kullanıcılar var. Önce kullanıcıların rollerini değiştirin.'], 422);
}
$role->delete();
return response()->json(['message' => 'Rol başarıyla silindi.']);
}
#[OA\Get(
path: '/api/admin/permissions',
summary: 'Tüm yetkileri listele',
description: 'Rol oluştururken/düzenlerken kullanılacak tüm mevcut yetkileri modül bazlı gruplandırarak döner.',
tags: ['Admin - Roles'],
security: [['sanctum' => []]],
responses: [new OA\Response(response: 200, description: 'Yetki listesi')],
)]
public function permissions(): JsonResponse
{
$permissions = Permission::query()
->where('guard_name', 'web')
->orderBy('name')
->pluck('name');
// Modül bazlı gruplandırma
$grouped = [];
foreach ($permissions as $permission) {
$parts = explode('-', $permission, 2);
if (count($parts) === 2) {
$grouped[$parts[1]][] = $permission;
}
}
return response()->json([
'data' => [
'all' => $permissions,
'grouped' => $grouped,
],
]);
}
}

View File

@@ -0,0 +1,135 @@
<?php
namespace App\Http\Controllers\Api\Admin;
use App\Actions\Schedule\CreateScheduleAction;
use App\Actions\Schedule\DeleteScheduleAction;
use App\Actions\Schedule\UpdateScheduleAction;
use App\DTOs\ScheduleData;
use App\Http\Controllers\Controller;
use App\Http\Requests\Schedule\StoreScheduleRequest;
use App\Http\Requests\Schedule\UpdateScheduleRequest;
use App\Http\Resources\CourseScheduleResource;
use App\Models\CourseSchedule;
use App\Repositories\Contracts\ScheduleRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use OpenApi\Attributes as OA;
class ScheduleController extends Controller
{
public function __construct(private ScheduleRepositoryInterface $repository) {}
#[OA\Get(
path: '/api/admin/schedules',
summary: 'Takvimleri listele (Admin)',
tags: ['Admin - Schedules'],
security: [['sanctum' => []]],
parameters: [
new OA\Parameter(name: 'course_id', in: 'query', required: false, schema: new OA\Schema(type: 'integer')),
new OA\Parameter(name: 'per_page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
],
responses: [new OA\Response(response: 200, description: 'Takvim listesi')],
)]
public function index(Request $request): AnonymousResourceCollection
{
$schedules = $this->repository->paginate(
$request->only(['course_id']),
$request->integer('per_page', 15),
);
return CourseScheduleResource::collection($schedules);
}
#[OA\Post(
path: '/api/admin/schedules',
summary: 'Yeni takvim oluştur',
tags: ['Admin - Schedules'],
security: [['sanctum' => []]],
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
required: ['course_id', 'start_date', 'location', 'quota'],
properties: [
new OA\Property(property: 'course_id', type: 'integer'),
new OA\Property(property: 'start_date', type: 'string', format: 'date'),
new OA\Property(property: 'end_date', type: 'string', format: 'date'),
new OA\Property(property: 'location', type: 'string'),
new OA\Property(property: 'instructor', type: 'string'),
new OA\Property(property: 'quota', type: 'integer'),
new OA\Property(property: 'enrolled_count', type: 'integer'),
new OA\Property(property: 'price_override', type: 'number'),
new OA\Property(property: 'status', type: 'string'),
new OA\Property(property: 'notes', type: 'string'),
],
)),
responses: [
new OA\Response(response: 201, description: 'Takvim oluşturuldu'),
new OA\Response(response: 422, description: 'Validasyon hatası'),
],
)]
public function store(StoreScheduleRequest $request, CreateScheduleAction $action): JsonResponse
{
$dto = ScheduleData::fromArray($request->validated());
$schedule = $action->execute($dto);
return response()->json(new CourseScheduleResource($schedule->load('course')), 201);
}
#[OA\Get(
path: '/api/admin/schedules/{schedule}',
summary: 'Takvim detayı (Admin)',
tags: ['Admin - Schedules'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'schedule', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
responses: [new OA\Response(response: 200, description: 'Takvim detayı')],
)]
public function show(CourseSchedule $schedule): JsonResponse
{
return response()->json(new CourseScheduleResource($schedule->load('course')));
}
#[OA\Put(
path: '/api/admin/schedules/{schedule}',
summary: 'Takvim güncelle',
tags: ['Admin - Schedules'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'schedule', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
properties: [
new OA\Property(property: 'course_id', type: 'integer'),
new OA\Property(property: 'start_date', type: 'string', format: 'date'),
new OA\Property(property: 'end_date', type: 'string', format: 'date'),
new OA\Property(property: 'location', type: 'string'),
new OA\Property(property: 'instructor', type: 'string'),
new OA\Property(property: 'quota', type: 'integer'),
new OA\Property(property: 'status', type: 'string'),
],
)),
responses: [
new OA\Response(response: 200, description: 'Takvim güncellendi'),
new OA\Response(response: 422, description: 'Validasyon hatası'),
],
)]
public function update(UpdateScheduleRequest $request, CourseSchedule $schedule, UpdateScheduleAction $action): JsonResponse
{
$dto = ScheduleData::fromArray(array_merge($schedule->toArray(), $request->validated()));
$schedule = $action->execute($schedule, $dto);
return response()->json(new CourseScheduleResource($schedule->load('course')));
}
#[OA\Delete(
path: '/api/admin/schedules/{schedule}',
summary: 'Takvim sil',
tags: ['Admin - Schedules'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'schedule', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
responses: [new OA\Response(response: 200, description: 'Takvim silindi')],
)]
public function destroy(CourseSchedule $schedule, DeleteScheduleAction $action): JsonResponse
{
$action->execute($schedule);
return response()->json(['message' => 'Takvim silindi.']);
}
}

View File

@@ -0,0 +1,89 @@
<?php
namespace App\Http\Controllers\Api\Admin;
use App\Actions\Setting\UpdateSettingsAction;
use App\Enums\SettingGroup;
use App\Http\Controllers\Controller;
use App\Http\Requests\Setting\UpdateSettingsRequest;
use App\Http\Resources\SettingResource;
use App\Repositories\Contracts\SettingRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use OpenApi\Attributes as OA;
class SettingController extends Controller
{
public function __construct(private SettingRepositoryInterface $repository) {}
#[OA\Get(
path: '/api/admin/settings',
summary: 'Tüm ayarları listele (Admin)',
tags: ['Admin - Settings'],
security: [['sanctum' => []]],
responses: [new OA\Response(response: 200, description: 'Ayar listesi')],
)]
public function index(): AnonymousResourceCollection
{
return SettingResource::collection($this->repository->all());
}
#[OA\Get(
path: '/api/admin/settings/group/{group}',
summary: 'Gruba göre ayarları getir',
tags: ['Admin - Settings'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'group', in: 'path', required: true, schema: new OA\Schema(type: 'string'))],
responses: [
new OA\Response(response: 200, description: 'Grup ayarları'),
new OA\Response(response: 404, description: 'Grup bulunamadı'),
],
)]
public function group(string $group): AnonymousResourceCollection
{
$settingGroup = SettingGroup::tryFrom($group);
if (! $settingGroup) {
abort(404, 'Ayar grubu bulunamadı.');
}
return SettingResource::collection($this->repository->getByGroup($settingGroup));
}
#[OA\Put(
path: '/api/admin/settings',
summary: 'Ayarları toplu güncelle (dot notation: general.site_name)',
tags: ['Admin - Settings'],
security: [['sanctum' => []]],
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
required: ['settings'],
properties: [
new OA\Property(property: 'settings', type: 'object', example: '{"general.site_name": "Yeni Ad", "contact.phone_primary": "+90 ..."}'),
],
)),
responses: [
new OA\Response(response: 200, description: 'Ayarlar güncellendi'),
new OA\Response(response: 422, description: 'Validasyon hatası'),
],
)]
public function update(UpdateSettingsRequest $request, UpdateSettingsAction $action): JsonResponse
{
$action->execute($request->validated('settings'));
return response()->json(['message' => 'Ayarlar güncellendi.']);
}
#[OA\Post(
path: '/api/admin/settings/clear-cache',
summary: 'Ayar cache temizle',
tags: ['Admin - Settings'],
security: [['sanctum' => []]],
responses: [new OA\Response(response: 200, description: 'Cache temizlendi')],
)]
public function clearCache(): JsonResponse
{
$this->repository->clearCache();
return response()->json(['message' => 'Ayar cache temizlendi.']);
}
}

View File

@@ -0,0 +1,138 @@
<?php
namespace App\Http\Controllers\Api\Admin;
use App\Http\Controllers\Controller;
use App\Http\Resources\StoryResource;
use App\Models\Story;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use OpenApi\Attributes as OA;
class StoryController extends Controller
{
#[OA\Get(
path: '/api/admin/stories',
summary: 'Tüm hikayeleri listele (Admin)',
tags: ['Admin - Stories'],
security: [['sanctum' => []]],
responses: [new OA\Response(response: 200, description: 'Hikaye listesi')],
)]
public function index(): AnonymousResourceCollection
{
return StoryResource::collection(
Story::query()->orderBy('order_index')->get()
);
}
#[OA\Post(
path: '/api/admin/stories',
summary: 'Yeni hikaye oluştur',
tags: ['Admin - Stories'],
security: [['sanctum' => []]],
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
required: ['title', 'content'],
properties: [
new OA\Property(property: 'title', type: 'string'),
new OA\Property(property: 'badge', type: 'string', nullable: true),
new OA\Property(property: 'content', type: 'string'),
new OA\Property(property: 'image', type: 'string', nullable: true),
new OA\Property(property: 'cta_text', type: 'string', nullable: true),
new OA\Property(property: 'cta_url', type: 'string', nullable: true),
new OA\Property(property: 'order_index', type: 'integer'),
new OA\Property(property: 'is_active', type: 'boolean'),
],
)),
responses: [
new OA\Response(response: 201, description: 'Hikaye oluşturuldu'),
new OA\Response(response: 422, description: 'Validasyon hatası'),
],
)]
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'title' => ['required', 'string', 'max:255'],
'badge' => ['nullable', 'string', 'max:100'],
'content' => ['required', 'string'],
'image' => ['nullable', 'string', 'max:255'],
'cta_text' => ['nullable', 'string', 'max:100'],
'cta_url' => ['nullable', 'string', 'max:255'],
'order_index' => ['sometimes', 'integer', 'min:0'],
'is_active' => ['sometimes', 'boolean'],
]);
$story = Story::create($validated);
return response()->json(new StoryResource($story), 201);
}
#[OA\Get(
path: '/api/admin/stories/{story}',
summary: 'Hikaye detayı',
tags: ['Admin - Stories'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'story', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
responses: [new OA\Response(response: 200, description: 'Hikaye detayı')],
)]
public function show(Story $story): JsonResponse
{
return response()->json(new StoryResource($story));
}
#[OA\Put(
path: '/api/admin/stories/{story}',
summary: 'Hikaye güncelle',
tags: ['Admin - Stories'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'story', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
properties: [
new OA\Property(property: 'title', type: 'string'),
new OA\Property(property: 'badge', type: 'string', nullable: true),
new OA\Property(property: 'content', type: 'string'),
new OA\Property(property: 'image', type: 'string', nullable: true),
new OA\Property(property: 'cta_text', type: 'string', nullable: true),
new OA\Property(property: 'cta_url', type: 'string', nullable: true),
new OA\Property(property: 'order_index', type: 'integer'),
new OA\Property(property: 'is_active', type: 'boolean'),
],
)),
responses: [
new OA\Response(response: 200, description: 'Hikaye güncellendi'),
new OA\Response(response: 422, description: 'Validasyon hatası'),
],
)]
public function update(Request $request, Story $story): JsonResponse
{
$validated = $request->validate([
'title' => ['sometimes', 'string', 'max:255'],
'badge' => ['nullable', 'string', 'max:100'],
'content' => ['sometimes', 'string'],
'image' => ['nullable', 'string', 'max:255'],
'cta_text' => ['nullable', 'string', 'max:100'],
'cta_url' => ['nullable', 'string', 'max:255'],
'order_index' => ['sometimes', 'integer', 'min:0'],
'is_active' => ['sometimes', 'boolean'],
]);
$story->update($validated);
return response()->json(new StoryResource($story->fresh()));
}
#[OA\Delete(
path: '/api/admin/stories/{story}',
summary: 'Hikaye sil',
tags: ['Admin - Stories'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'story', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
responses: [new OA\Response(response: 200, description: 'Hikaye silindi')],
)]
public function destroy(Story $story): JsonResponse
{
$story->delete();
return response()->json(['message' => 'Hikaye silindi.']);
}
}

View File

@@ -0,0 +1,138 @@
<?php
namespace App\Http\Controllers\Api\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use OpenApi\Attributes as OA;
class UploadController extends Controller
{
#[OA\Post(
path: '/api/admin/upload',
summary: 'Dosya yükle',
description: 'Görsel veya video dosyası yükler. Görseller max 5MB, videolar max 100MB. Modül bazlı klasörleme destekler.',
security: [['sanctum' => []]],
tags: ['Upload'],
requestBody: new OA\RequestBody(
required: true,
content: new OA\MediaType(
mediaType: 'multipart/form-data',
schema: new OA\Schema(
required: ['file'],
properties: [
new OA\Property(property: 'file', type: 'string', format: 'binary', description: 'Görsel veya video dosyası'),
new OA\Property(property: 'type', type: 'string', enum: ['image', 'video'], description: 'Dosya tipi'),
new OA\Property(property: 'folder', type: 'string', description: 'Modül klasörü (hero-slides, courses, vb.)'),
],
),
),
),
responses: [
new OA\Response(response: 201, description: 'Dosya yüklendi', content: new OA\JsonContent(
properties: [
new OA\Property(property: 'data', type: 'object', properties: [
new OA\Property(property: 'path', type: 'string'),
new OA\Property(property: 'url', type: 'string'),
]),
],
)),
new OA\Response(response: 422, description: 'Validasyon hatası'),
],
)]
public function store(Request $request): JsonResponse
{
$type = $request->input('type', 'image');
if ($type === 'video') {
return $this->storeVideo($request);
}
return $this->storeImage($request);
}
/**
* @var list<string>
*/
private const ALLOWED_FOLDERS = [
'images',
'videos',
'hero-slides',
'settings',
'pages',
'courses',
'announcements',
'categories',
];
private function resolveFolder(Request $request, string $default): string
{
$folder = $request->input('folder', $default);
if (! in_array($folder, self::ALLOWED_FOLDERS, true)) {
$folder = $default;
}
return $folder;
}
private function storeImage(Request $request): JsonResponse
{
$request->validate([
'file' => ['required', 'file', 'image', 'max:5120'],
'folder' => ['sometimes', 'string'],
], [
'file.required' => 'Dosya zorunludur.',
'file.image' => 'Dosya bir görsel (jpg, png, gif, svg, webp) olmalıdır.',
'file.max' => 'Dosya boyutu en fazla 5MB olabilir.',
]);
$folder = $this->resolveFolder($request, 'images');
$file = $request->file('file');
$filename = Str::uuid().'.'.$file->getClientOriginalExtension();
$directory = public_path('uploads/'.$folder);
$file->move($directory, $filename);
$relativePath = 'uploads/'.$folder.'/'.$filename;
return response()->json([
'data' => [
'path' => $relativePath,
'url' => url($relativePath),
],
], 201);
}
private function storeVideo(Request $request): JsonResponse
{
$request->validate([
'file' => ['required', 'file', 'mimes:mp4,webm,mov,avi,mkv', 'max:102400'],
'folder' => ['sometimes', 'string'],
], [
'file.required' => 'Dosya zorunludur.',
'file.mimes' => 'Dosya bir video (mp4, webm, mov, avi, mkv) olmalıdır.',
'file.max' => 'Video boyutu en fazla 100MB olabilir.',
]);
$folder = $this->resolveFolder($request, 'videos');
$file = $request->file('file');
$filename = Str::uuid().'.'.$file->getClientOriginalExtension();
$directory = public_path('uploads/'.$folder);
$file->move($directory, $filename);
$relativePath = 'uploads/'.$folder.'/'.$filename;
return response()->json([
'data' => [
'path' => $relativePath,
'url' => url($relativePath),
],
], 201);
}
}

View File

@@ -0,0 +1,147 @@
<?php
namespace App\Http\Controllers\Api\Admin;
use App\Actions\User\CreateUserAction;
use App\Actions\User\DeleteUserAction;
use App\Actions\User\UpdateUserAction;
use App\DTOs\UserData;
use App\Http\Controllers\Controller;
use App\Http\Requests\User\StoreUserRequest;
use App\Http\Requests\User\UpdateUserRequest;
use App\Http\Resources\UserResource;
use App\Models\User;
use App\Repositories\Contracts\UserRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use OpenApi\Attributes as OA;
class UserController extends Controller
{
public function __construct(private UserRepositoryInterface $repository) {}
#[OA\Get(
path: '/api/admin/users',
summary: 'Admin kullanıcılarını listele',
tags: ['Admin - Users'],
security: [['sanctum' => []]],
parameters: [
new OA\Parameter(name: 'search', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'role', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'per_page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
],
responses: [new OA\Response(response: 200, description: 'Kullanıcı listesi')],
)]
public function index(Request $request): AnonymousResourceCollection
{
$users = $this->repository->paginate(
filters: $request->only('search', 'role'),
perPage: $request->integer('per_page', 15),
);
return UserResource::collection($users);
}
#[OA\Post(
path: '/api/admin/users',
summary: 'Yeni admin kullanıcı oluştur',
tags: ['Admin - Users'],
security: [['sanctum' => []]],
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(
required: ['name', 'email', 'password', 'password_confirmation', 'role'],
properties: [
new OA\Property(property: 'name', type: 'string', example: 'Editör Kullanıcı'),
new OA\Property(property: 'email', type: 'string', format: 'email', example: 'editor@bogazici.com'),
new OA\Property(property: 'password', type: 'string', format: 'password', example: 'password123'),
new OA\Property(property: 'password_confirmation', type: 'string', format: 'password', example: 'password123'),
new OA\Property(property: 'role', type: 'string', example: 'editor'),
],
),
),
responses: [
new OA\Response(response: 201, description: 'Kullanıcı oluşturuldu'),
new OA\Response(response: 422, description: 'Validasyon hatası'),
],
)]
public function store(StoreUserRequest $request, CreateUserAction $action): JsonResponse
{
$dto = UserData::fromArray($request->validated());
$user = $action->execute($dto);
return (new UserResource($user))
->response()
->setStatusCode(201);
}
#[OA\Get(
path: '/api/admin/users/{user}',
summary: 'Kullanıcı detayı',
tags: ['Admin - Users'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'user', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
responses: [
new OA\Response(response: 200, description: 'Kullanıcı detayı'),
new OA\Response(response: 404, description: 'Bulunamadı'),
],
)]
public function show(User $user): UserResource
{
$user->load('roles');
return new UserResource($user);
}
#[OA\Put(
path: '/api/admin/users/{user}',
summary: 'Kullanıcı güncelle',
tags: ['Admin - Users'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'user', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
properties: [
new OA\Property(property: 'name', type: 'string'),
new OA\Property(property: 'email', type: 'string', format: 'email'),
new OA\Property(property: 'password', type: 'string', format: 'password'),
new OA\Property(property: 'password_confirmation', type: 'string', format: 'password'),
new OA\Property(property: 'role', type: 'string', example: 'editor'),
],
)),
responses: [
new OA\Response(response: 200, description: 'Kullanıcı güncellendi'),
new OA\Response(response: 422, description: 'Validasyon hatası'),
],
)]
public function update(UpdateUserRequest $request, User $user, UpdateUserAction $action): UserResource
{
$dto = UserData::fromArray($request->validated());
$user = $action->execute($user, $dto);
return new UserResource($user);
}
#[OA\Delete(
path: '/api/admin/users/{user}',
summary: 'Kullanıcı sil (soft delete)',
tags: ['Admin - Users'],
security: [['sanctum' => []]],
parameters: [new OA\Parameter(name: 'user', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
responses: [
new OA\Response(response: 200, description: 'Kullanıcı silindi'),
new OA\Response(response: 403, description: 'Kendini silemezsin'),
new OA\Response(response: 404, description: 'Bulunamadı'),
],
)]
public function destroy(User $user, DeleteUserAction $action): JsonResponse
{
if ($user->id === auth()->id()) {
return response()->json(['message' => 'Kendinizi silemezsiniz.'], 403);
}
$action->execute($user);
return response()->json(['message' => 'Kullanıcı başarıyla silindi.']);
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Http\Resources\AnnouncementResource;
use App\Repositories\Contracts\AnnouncementRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use OpenApi\Attributes as OA;
class AnnouncementController extends Controller
{
public function __construct(private AnnouncementRepositoryInterface $repository) {}
#[OA\Get(
path: '/api/v1/announcements',
summary: 'Duyuruları listele',
tags: ['Announcements'],
parameters: [
new OA\Parameter(name: 'category', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'featured', in: 'query', required: false, schema: new OA\Schema(type: 'boolean')),
new OA\Parameter(name: 'search', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'sort', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'per_page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
],
responses: [new OA\Response(response: 200, description: 'Duyuru listesi')],
)]
public function index(Request $request): AnonymousResourceCollection
{
$announcements = $this->repository->paginate(
$request->only(['category', 'featured', 'search', 'sort']),
$request->integer('per_page', 15),
);
return AnnouncementResource::collection($announcements);
}
#[OA\Get(
path: '/api/v1/announcements/{slug}',
summary: 'Duyuru detayı',
tags: ['Announcements'],
parameters: [new OA\Parameter(name: 'slug', in: 'path', required: true, schema: new OA\Schema(type: 'string'))],
responses: [
new OA\Response(response: 200, description: 'Duyuru detayı'),
new OA\Response(response: 404, description: 'Duyuru bulunamadı'),
],
)]
public function show(string $slug): JsonResponse
{
$announcement = $this->repository->findBySlug($slug);
if (! $announcement) {
return response()->json(['message' => 'Duyuru bulunamadı.'], 404);
}
$announcement->load(['comments' => fn ($q) => $q->where('is_approved', true)->latest()]);
return response()->json(new AnnouncementResource($announcement));
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Http\Resources\CategoryResource;
use App\Repositories\Contracts\CategoryRepositoryInterface;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use OpenApi\Attributes as OA;
class CategoryController extends Controller
{
public function __construct(private CategoryRepositoryInterface $repository) {}
#[OA\Get(
path: '/api/v1/categories',
summary: 'Kategorileri listele',
description: 'Tüm aktif kategorileri sayfalanmış olarak döndürür.',
tags: ['Categories'],
parameters: [
new OA\Parameter(name: 'search', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'per_page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
],
responses: [
new OA\Response(response: 200, description: 'Kategori listesi'),
],
)]
public function index(Request $request): AnonymousResourceCollection
{
$categories = $this->repository->paginate(
filters: $request->only('search'),
perPage: $request->integer('per_page', 15),
);
return CategoryResource::collection($categories);
}
#[OA\Get(
path: '/api/v1/categories/{slug}',
summary: 'Kategori detayı',
description: 'Slug ile kategori detayını döndürür.',
tags: ['Categories'],
parameters: [
new OA\Parameter(name: 'slug', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(response: 200, description: 'Kategori detayı'),
new OA\Response(response: 404, description: 'Kategori bulunamadı'),
],
)]
public function show(string $slug): CategoryResource
{
$category = $this->repository->findBySlug($slug);
abort_if(! $category, 404, 'Kategori bulunamadı.');
return new CategoryResource($category);
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Actions\Comment\CreateCommentAction;
use App\DTOs\CommentData;
use App\Http\Controllers\Controller;
use App\Http\Requests\Comment\StoreCommentRequest;
use App\Http\Resources\CommentResource;
use App\Models\Announcement;
use App\Models\Category;
use App\Models\Course;
use App\Repositories\Contracts\CommentRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use OpenApi\Attributes as OA;
class CommentController extends Controller
{
/**
* Morph map for commentable types from the request.
*
* @var array<string, string>
*/
private const COMMENTABLE_MAP = [
'course' => Course::class,
'category' => Category::class,
'announcement' => Announcement::class,
];
public function __construct(private CommentRepositoryInterface $repository) {}
#[OA\Get(
path: '/api/v1/comments/{type}/{id}',
summary: 'Onaylı yorumları getir',
tags: ['Comments'],
parameters: [
new OA\Parameter(name: 'type', in: 'path', required: true, schema: new OA\Schema(type: 'string', enum: ['course', 'category', 'announcement'])),
new OA\Parameter(name: 'id', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
],
responses: [
new OA\Response(response: 200, description: 'Onaylı yorum listesi'),
new OA\Response(response: 404, description: 'Geçersiz yorum tipi'),
],
)]
public function index(string $type, int $id): AnonymousResourceCollection|JsonResponse
{
$commentableType = self::COMMENTABLE_MAP[$type] ?? null;
if ($commentableType === null) {
return response()->json(['message' => 'Geçersiz yorum tipi.'], 404);
}
$comments = $this->repository->getApprovedByCommentable($commentableType, $id);
return CommentResource::collection($comments);
}
#[OA\Post(
path: '/api/v1/comments',
summary: 'Yorum gönder',
tags: ['Comments'],
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(
required: ['body', 'author_name', 'author_email', 'commentable_type', 'commentable_id'],
properties: [
new OA\Property(property: 'body', type: 'string'),
new OA\Property(property: 'author_name', type: 'string'),
new OA\Property(property: 'author_email', type: 'string', format: 'email'),
new OA\Property(property: 'commentable_type', type: 'string', enum: ['course', 'category', 'announcement']),
new OA\Property(property: 'commentable_id', type: 'integer'),
new OA\Property(property: 'rating', type: 'integer', minimum: 1, maximum: 5),
],
),
),
responses: [
new OA\Response(response: 201, description: 'Yorum gönderildi'),
new OA\Response(response: 422, description: 'Validasyon hatası'),
],
)]
public function store(StoreCommentRequest $request, CreateCommentAction $action): JsonResponse
{
$validated = $request->validated();
$validated['commentable_type'] = self::COMMENTABLE_MAP[$validated['commentable_type']] ?? $validated['commentable_type'];
$dto = CommentData::fromArray($validated);
$action->execute($dto);
return response()->json(['message' => 'Yorumunuz incelenmek üzere gönderildi.'], 201);
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Http\Resources\CourseResource;
use App\Repositories\Contracts\CourseRepositoryInterface;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use OpenApi\Attributes as OA;
class CourseController extends Controller
{
public function __construct(private CourseRepositoryInterface $repository) {}
#[OA\Get(
path: '/api/v1/courses',
summary: 'Eğitimleri listele',
description: 'Tüm eğitimleri sayfalanmış olarak döndürür. Kategori, arama ve sıralama filtresi destekler.',
tags: ['Courses'],
parameters: [
new OA\Parameter(name: 'category', in: 'query', required: false, schema: new OA\Schema(type: 'string'), description: 'Kategori slug filtresi'),
new OA\Parameter(name: 'search', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'sort', in: 'query', required: false, schema: new OA\Schema(type: 'string'), description: 'Örn: -created_at, title'),
new OA\Parameter(name: 'per_page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
],
responses: [
new OA\Response(response: 200, description: 'Eğitim listesi'),
],
)]
public function index(Request $request): AnonymousResourceCollection
{
$courses = $this->repository->paginate(
filters: $request->only('category', 'search', 'sort'),
perPage: $request->integer('per_page', 15),
);
return CourseResource::collection($courses);
}
#[OA\Get(
path: '/api/v1/courses/{slug}',
summary: 'Eğitim detayı',
description: 'Slug ile eğitim detayını döndürür.',
tags: ['Courses'],
parameters: [
new OA\Parameter(name: 'slug', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(response: 200, description: 'Eğitim detayı'),
new OA\Response(response: 404, description: 'Eğitim bulunamadı'),
],
)]
public function show(string $slug): CourseResource
{
$course = $this->repository->findBySlug($slug);
abort_if(! $course, 404, 'Eğitim bulunamadı.');
return new CourseResource($course);
}
}

Some files were not shown because too many files have changed in this diff Show More