Initial commit of existing project
This commit is contained in:
parent
ec8ffc7716
commit
3013bb5740
18
.editorconfig
Normal file
18
.editorconfig
Normal 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
|
||||
70
.env.example
Normal file
70
.env.example
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
APP_NAME=Laravel
|
||||
APP_ENV=local
|
||||
APP_KEY=base64:E0+Ieqm3EN0qO5yMEFEkXcpTY0FhglNAZUlHdEfn2eU=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost:8000
|
||||
|
||||
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
|
||||
|
||||
# Movies / OMDb
|
||||
OMDB_API_KEY=
|
||||
OMDB_LANGUAGE=en-US
|
||||
OMDB_CACHE_TTL=3600
|
||||
|
||||
DB_CONNECTION=sqlite
|
||||
# DB_HOST=127.0.0.1
|
||||
# DB_PORT=3306
|
||||
# DB_DATABASE=laravel
|
||||
# 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
11
.gitattributes
vendored
Normal 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
|
||||
|
||||
CHANGELOG.md export-ignore
|
||||
README.md export-ignore
|
||||
.github/workflows/browser-tests.yml export-ignore
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -34,3 +34,4 @@ docs/_book
|
|||
# TODO: where does this rule come from?
|
||||
test/
|
||||
|
||||
.idea/
|
||||
|
|
|
|||
2
.prettierignore
Normal file
2
.prettierignore
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
resources/js/components/ui/*
|
||||
resources/views/mail/*
|
||||
26
.prettierrc
Normal file
26
.prettierrc
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"singleAttributePerLine": false,
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"printWidth": 80,
|
||||
"plugins": [
|
||||
"prettier-plugin-organize-imports",
|
||||
"prettier-plugin-tailwindcss"
|
||||
],
|
||||
"tailwindFunctions": [
|
||||
"clsx",
|
||||
"cn",
|
||||
"cva"
|
||||
],
|
||||
"tailwindStylesheet": "resources/css/app.css",
|
||||
"tabWidth": 4,
|
||||
"overrides": [
|
||||
{
|
||||
"files": "**/*.yml",
|
||||
"options": {
|
||||
"tabWidth": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
205
README.md
205
README.md
|
|
@ -1,3 +1,204 @@
|
|||
# PIMS
|
||||
# PIMS — Personal Inventory Management System
|
||||
|
||||
Personal Inventory Management System
|
||||
PIMS is a Laravel + Inertia.js + Vue 3 application intended to manage personal inventory. This repository is a modern Laravel 12 starter that pairs a PHP backend with a TypeScript/Vue frontend, bundled via Vite and styled with Tailwind CSS.
|
||||
|
||||
Note: This README was updated to reflect the current codebase. Where details are unclear in the repository, TODOs are explicitly noted for maintainers to fill in.
|
||||
|
||||
## Overview
|
||||
|
||||
- Backend: Laravel Framework ^12.0 (PHP ^8.2)
|
||||
- Inertia.js (server: `inertiajs/inertia-laravel` ^2.x)
|
||||
- Frontend: Vue 3, TypeScript, Vite 7, Tailwind CSS 4
|
||||
- Auth: Laravel Fortify
|
||||
- Dev tooling: Laravel Boost, Pail (logs), Pint (code style), ESLint, Prettier
|
||||
- Package managers: Composer (PHP) and npm (Node)
|
||||
- Default entry point: `public/index.php`
|
||||
|
||||
## Requirements
|
||||
|
||||
- PHP 8.2+
|
||||
- Composer 2.x
|
||||
- Node.js 18+ (recommended LTS) and npm 9+
|
||||
- SQLite (default on first-run) or another database supported by Laravel
|
||||
- The project creates `database/database.sqlite` on project creation and runs migrations automatically in some script flows.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### 1) Clone and configure
|
||||
|
||||
```
|
||||
git clone <YOUR_REPO_URL> pims
|
||||
cd pims
|
||||
```
|
||||
|
||||
Create your environment file and app key, install dependencies, run migrations, and build assets. The Composer `setup` script automates these steps:
|
||||
|
||||
```
|
||||
composer run setup
|
||||
```
|
||||
|
||||
What `composer run setup` does:
|
||||
- `composer install`
|
||||
- Copy `.env.example` to `.env` if it does not exist
|
||||
- `php artisan key:generate`
|
||||
- `php artisan migrate --force`
|
||||
- `npm install`
|
||||
- `npm run build`
|
||||
|
||||
If you prefer to do these manually:
|
||||
|
||||
```
|
||||
cp .env.example .env # if .env.example is populated; see TODO below
|
||||
php artisan key:generate
|
||||
composer install
|
||||
php artisan migrate
|
||||
npm install
|
||||
npm run dev # or npm run build
|
||||
```
|
||||
|
||||
### 2) Run the app in development
|
||||
|
||||
Option A — Single terminal processes:
|
||||
|
||||
```
|
||||
php artisan serve
|
||||
php artisan queue:listen --tries=1 # optional if you use queues
|
||||
php artisan pail --timeout=0 # optional live log viewer
|
||||
npm run dev # Vite dev server for assets
|
||||
```
|
||||
|
||||
Option B — Use the combined dev script (runs server, queue, logs, and Vite together):
|
||||
|
||||
```
|
||||
composer run dev
|
||||
```
|
||||
|
||||
Server will be available at the host/port shown in `php artisan serve` output (typically http://127.0.0.1:8000). The default route `/` renders an Inertia page `Welcome`. The `dashboard` route is protected by `auth` and `verified` middleware.
|
||||
|
||||
### Optional: Inertia SSR (Server-Side Rendering)
|
||||
|
||||
This project includes scripts to build SSR bundles and start the Inertia SSR server.
|
||||
|
||||
```
|
||||
composer run dev:ssr # will run php server, queue, logs, and the Inertia SSR server
|
||||
```
|
||||
|
||||
Or build SSR assets without starting the SSR process:
|
||||
|
||||
```
|
||||
npm run build:ssr
|
||||
```
|
||||
|
||||
## Scripts
|
||||
|
||||
Composer:
|
||||
- `composer run setup` — Full setup: install PHP deps, ensure `.env`, generate key, migrate, install Node deps, build assets
|
||||
- `composer run dev` — Concurrent dev processes (server, queue worker, logs, Vite)
|
||||
- `composer run dev:ssr` — Build SSR assets and run server + queue + logs + Inertia SSR
|
||||
- `composer run test` — Clear config cache and run tests via `php artisan test`
|
||||
|
||||
npm:
|
||||
- `npm run dev` — Vite dev server
|
||||
- `npm run build` — Build production assets
|
||||
- `npm run build:ssr` — Build production assets and SSR bundle
|
||||
- `npm run lint` — ESLint with auto-fix
|
||||
- `npm run format` — Prettier write on `resources/`
|
||||
- `npm run format:check` — Prettier check
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- The repository contains an `.env.example` file, but it is currently empty.
|
||||
- TODO: Populate `.env.example` with the required variables for this app (e.g., `APP_NAME`, `APP_ENV`, `APP_KEY`, database config, mail config, etc.).
|
||||
- On first setup, the scripts will generate `APP_KEY` automatically.
|
||||
- Database: the project’s Composer post-create hook creates `database/database.sqlite` and runs migrations; you can continue with SQLite (set `DB_CONNECTION=sqlite`) or configure MySQL/PostgreSQL/etc. in `.env`.
|
||||
|
||||
## Project Structure
|
||||
|
||||
High-level directories of interest:
|
||||
|
||||
- `app/` — Laravel application code (models, jobs, controllers, etc.)
|
||||
- `bootstrap/` — Laravel bootstrapping
|
||||
- `config/` — Configuration files
|
||||
- `database/` — Migrations, seeders; SQLite file may live here (`database.sqlite`)
|
||||
- `public/` — Web server document root; main PHP entry point is `public/index.php`
|
||||
- `resources/` — Frontend assets (Vue, TypeScript, styles)
|
||||
- `routes/` — Route definitions (e.g., `web.php` renders Inertia pages)
|
||||
- `storage/` — Framework storage (logs, cache, sessions)
|
||||
- `tests/` — Test suite (Pest + Laravel plugin)
|
||||
- `vite.config.ts` — Vite build configuration
|
||||
- `composer.json` — PHP dependencies and Composer scripts
|
||||
- `package.json` — Node dependencies and npm scripts
|
||||
|
||||
## Modules
|
||||
|
||||
This application is organized to support feature modules under `app/Modules/` (e.g., Movies, Books, Games, Collectibles). Each module owns its domain logic and ships with its own `README.md` documenting endpoints, data models, services/contracts, environment variables, and development notes.
|
||||
|
||||
- Current module(s):
|
||||
- Movies — see `app/Modules/Movies/README.md` for detailed documentation.
|
||||
|
||||
As additional modules are added (Books, Games, Collectibles, etc.), refer to their respective `app/Modules/<Module>/README.md` files for module-specific guidance.
|
||||
|
||||
## Running Tests
|
||||
|
||||
You can run tests with either Composer or Artisan directly:
|
||||
|
||||
```
|
||||
composer run test
|
||||
# or
|
||||
php artisan test
|
||||
```
|
||||
|
||||
The project is configured for Pest with `pestphp/pest` and `pest-plugin-laravel` in `require-dev`.
|
||||
|
||||
## Database & Migrations
|
||||
|
||||
Run pending migrations:
|
||||
|
||||
```
|
||||
php artisan migrate
|
||||
```
|
||||
|
||||
Seeders/factories are available under `database/` if defined.
|
||||
|
||||
Default behavior suggests SQLite can be used out of the box. To switch to another DB, update your `.env` accordingly and re-run migrations.
|
||||
|
||||
## Linting & Formatting
|
||||
|
||||
```
|
||||
npm run lint # ESLint
|
||||
npm run format # Prettier write
|
||||
npm run format:check # Prettier check
|
||||
```
|
||||
|
||||
PHP code style is managed by Laravel Pint (installed as a dev dependency). You can run it via:
|
||||
|
||||
```
|
||||
./vendor/bin/pint
|
||||
```
|
||||
|
||||
## Development Notes
|
||||
|
||||
- Logs: `php artisan pail --timeout=0` starts a live log viewer.
|
||||
- Queues: `php artisan queue:listen --tries=1` is used in the dev script.
|
||||
- Docker: Laravel Sail is present as a dev dependency, but the repo does not include a Sail config section here.
|
||||
- TODO: Confirm whether Sail should be the preferred local environment and document the `./vendor/bin/sail` workflow if applicable.
|
||||
|
||||
## Deployment
|
||||
|
||||
General Laravel deployment notes:
|
||||
|
||||
1. Ensure environment variables are configured and `APP_KEY` is set.
|
||||
2. Run migrations: `php artisan migrate --force`.
|
||||
3. Build assets: `npm ci && npm run build`.
|
||||
4. Cache optimizations (optional):
|
||||
- `php artisan config:cache`
|
||||
- `php artisan route:cache`
|
||||
- `php artisan view:cache`
|
||||
|
||||
TODO: Add specific deployment instructions (hosting provider, CI/CD, SSR process, queue workers, scheduler) once decided.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License (per `composer.json`).
|
||||
|
||||
TODO: If a `LICENSE` file is missing in the repository root, add one with the MIT license text.
|
||||
|
|
|
|||
39
app/Actions/Fortify/CreateNewUser.php
Normal file
39
app/Actions/Fortify/CreateNewUser.php
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Laravel\Fortify\Contracts\CreatesNewUsers;
|
||||
|
||||
class CreateNewUser implements CreatesNewUsers
|
||||
{
|
||||
use PasswordValidationRules;
|
||||
|
||||
/**
|
||||
* Validate and create a newly registered user.
|
||||
*
|
||||
* @param array<string, string> $input
|
||||
*/
|
||||
public function create(array $input): User
|
||||
{
|
||||
Validator::make($input, [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'email',
|
||||
'max:255',
|
||||
Rule::unique(User::class),
|
||||
],
|
||||
'password' => $this->passwordRules(),
|
||||
])->validate();
|
||||
|
||||
return User::create([
|
||||
'name' => $input['name'],
|
||||
'email' => $input['email'],
|
||||
'password' => $input['password'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
18
app/Actions/Fortify/PasswordValidationRules.php
Normal file
18
app/Actions/Fortify/PasswordValidationRules.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
trait PasswordValidationRules
|
||||
{
|
||||
/**
|
||||
* Get the validation rules used to validate passwords.
|
||||
*
|
||||
* @return array<int, \Illuminate\Contracts\Validation\Rule|array<mixed>|string>
|
||||
*/
|
||||
protected function passwordRules(): array
|
||||
{
|
||||
return ['required', 'string', Password::default(), 'confirmed'];
|
||||
}
|
||||
}
|
||||
28
app/Actions/Fortify/ResetUserPassword.php
Normal file
28
app/Actions/Fortify/ResetUserPassword.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Laravel\Fortify\Contracts\ResetsUserPasswords;
|
||||
|
||||
class ResetUserPassword implements ResetsUserPasswords
|
||||
{
|
||||
use PasswordValidationRules;
|
||||
|
||||
/**
|
||||
* Validate and reset the user's forgotten password.
|
||||
*
|
||||
* @param array<string, string> $input
|
||||
*/
|
||||
public function reset(User $user, array $input): void
|
||||
{
|
||||
Validator::make($input, [
|
||||
'password' => $this->passwordRules(),
|
||||
])->validate();
|
||||
|
||||
$user->forceFill([
|
||||
'password' => $input['password'],
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
8
app/Http/Controllers/Controller.php
Normal file
8
app/Http/Controllers/Controller.php
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
38
app/Http/Controllers/Settings/PasswordController.php
Normal file
38
app/Http/Controllers/Settings/PasswordController.php
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Settings;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class PasswordController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show the user's password settings page.
|
||||
*/
|
||||
public function edit(): Response
|
||||
{
|
||||
return Inertia::render('settings/Password');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user's password.
|
||||
*/
|
||||
public function update(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'current_password' => ['required', 'current_password'],
|
||||
'password' => ['required', Password::defaults(), 'confirmed'],
|
||||
]);
|
||||
|
||||
$request->user()->update([
|
||||
'password' => $validated['password'],
|
||||
]);
|
||||
|
||||
return back();
|
||||
}
|
||||
}
|
||||
63
app/Http/Controllers/Settings/ProfileController.php
Normal file
63
app/Http/Controllers/Settings/ProfileController.php
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Settings;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Settings\ProfileUpdateRequest;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show the user's profile settings page.
|
||||
*/
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
return Inertia::render('settings/Profile', [
|
||||
'mustVerifyEmail' => $request->user() instanceof MustVerifyEmail,
|
||||
'status' => $request->session()->get('status'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user's profile information.
|
||||
*/
|
||||
public function update(ProfileUpdateRequest $request): RedirectResponse
|
||||
{
|
||||
$request->user()->fill($request->validated());
|
||||
|
||||
if ($request->user()->isDirty('email')) {
|
||||
$request->user()->email_verified_at = null;
|
||||
}
|
||||
|
||||
$request->user()->save();
|
||||
|
||||
return to_route('profile.edit');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the user's profile.
|
||||
*/
|
||||
public function destroy(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'password' => ['required', 'current_password'],
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
Auth::logout();
|
||||
|
||||
$user->delete();
|
||||
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect('/');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Settings;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Settings\TwoFactorAuthenticationRequest;
|
||||
use Illuminate\Routing\Controllers\HasMiddleware;
|
||||
use Illuminate\Routing\Controllers\Middleware;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use Laravel\Fortify\Features;
|
||||
|
||||
class TwoFactorAuthenticationController extends Controller implements HasMiddleware
|
||||
{
|
||||
/**
|
||||
* Get the middleware that should be assigned to the controller.
|
||||
*/
|
||||
public static function middleware(): array
|
||||
{
|
||||
return Features::optionEnabled(Features::twoFactorAuthentication(), 'confirmPassword')
|
||||
? [new Middleware('password.confirm', only: ['show'])]
|
||||
: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the user's two-factor authentication settings page.
|
||||
*/
|
||||
public function show(TwoFactorAuthenticationRequest $request): Response
|
||||
{
|
||||
$request->ensureStateIsValid();
|
||||
|
||||
return Inertia::render('settings/TwoFactor', [
|
||||
'twoFactorEnabled' => $request->user()->hasEnabledTwoFactorAuthentication(),
|
||||
'requiresConfirmation' => Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
23
app/Http/Middleware/HandleAppearance.php
Normal file
23
app/Http/Middleware/HandleAppearance.php
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class HandleAppearance
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
View::share('appearance', $request->cookie('appearance') ?? 'system');
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
51
app/Http/Middleware/HandleInertiaRequests.php
Normal file
51
app/Http/Middleware/HandleInertiaRequests.php
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Foundation\Inspiring;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Middleware;
|
||||
|
||||
class HandleInertiaRequests extends Middleware
|
||||
{
|
||||
/**
|
||||
* The root template that's loaded on the first page visit.
|
||||
*
|
||||
* @see https://inertiajs.com/server-side-setup#root-template
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rootView = 'app';
|
||||
|
||||
/**
|
||||
* Determines the current asset version.
|
||||
*
|
||||
* @see https://inertiajs.com/asset-versioning
|
||||
*/
|
||||
public function version(Request $request): ?string
|
||||
{
|
||||
return parent::version($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the props that are shared by default.
|
||||
*
|
||||
* @see https://inertiajs.com/shared-data
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function share(Request $request): array
|
||||
{
|
||||
[$message, $author] = str(Inspiring::quotes()->random())->explode('-');
|
||||
|
||||
return [
|
||||
...parent::share($request),
|
||||
'name' => config('app.name'),
|
||||
'quote' => ['message' => trim($message), 'author' => trim($author)],
|
||||
'auth' => [
|
||||
'user' => $request->user(),
|
||||
],
|
||||
'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true',
|
||||
];
|
||||
}
|
||||
}
|
||||
30
app/Http/Requests/Settings/ProfileUpdateRequest.php
Normal file
30
app/Http/Requests/Settings/ProfileUpdateRequest.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests\Settings;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class ProfileUpdateRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'lowercase',
|
||||
'email',
|
||||
'max:255',
|
||||
Rule::unique(User::class)->ignore($this->user()->id),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests\Settings;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Laravel\Fortify\Features;
|
||||
use Laravel\Fortify\InteractsWithTwoFactorState;
|
||||
|
||||
class TwoFactorAuthenticationRequest extends FormRequest
|
||||
{
|
||||
use InteractsWithTwoFactorState;
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return Features::enabled(Features::twoFactorAuthentication());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
52
app/Models/User.php
Normal file
52
app/Models/User.php
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasFactory, Notifiable, TwoFactorAuthenticatable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'two_factor_secret',
|
||||
'two_factor_recovery_codes',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'two_factor_confirmed_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Modules\Movies\Http\Requests\AcceptMovieRequest;
|
||||
use App\Modules\Movies\Services\Contracts\MovieProvider;
|
||||
use App\Modules\Movies\Services\Contracts\UpsertMovieServiceInterface;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class AcceptMovieController extends Controller
|
||||
{
|
||||
public function store(
|
||||
AcceptMovieRequest $request,
|
||||
MovieProvider $provider,
|
||||
UpsertMovieServiceInterface $upserter
|
||||
): JsonResponse {
|
||||
$providerId = $request->validated('provider_id');
|
||||
$mode = $request->validated('mode'); // overwrite|duplicate
|
||||
|
||||
$details = $provider->details($providerId);
|
||||
|
||||
$movie = $upserter->handle($details, $mode);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'ok',
|
||||
'movie_id' => $movie->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Modules\Movies\Http\Requests\DestroyMovieRequest;
|
||||
use App\Modules\Movies\Services\Contracts\DeleteMovieServiceInterface;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class DeleteMovieController extends Controller
|
||||
{
|
||||
public function destroy(int $movie, DestroyMovieRequest $request, DeleteMovieServiceInterface $service): JsonResponse
|
||||
{
|
||||
$service->handle($movie);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'ok',
|
||||
'deleted' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class EditMoviePageController extends Controller
|
||||
{
|
||||
public function __invoke(string $movie): Response
|
||||
{
|
||||
return Inertia::render('admin/movies/Edit', [
|
||||
'movieId' => (int) $movie,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Modules\Movies\Http\Requests\ExistsMovieRequest;
|
||||
use App\Modules\Movies\Services\Contracts\CheckMovieExistsServiceInterface;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class ExistsMovieController extends Controller
|
||||
{
|
||||
public function __invoke(
|
||||
ExistsMovieRequest $request,
|
||||
CheckMovieExistsServiceInterface $service
|
||||
): JsonResponse {
|
||||
$provider = $request->validated('provider') ?? 'omdb';
|
||||
$providerId = $request->validated('provider_id');
|
||||
|
||||
$movie = $service->findByProviderId($provider, $providerId);
|
||||
|
||||
return response()->json([
|
||||
'exists' => (bool) $movie,
|
||||
'movie_id' => $movie?->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class MoviesListPageController extends Controller
|
||||
{
|
||||
public function __invoke(): Response
|
||||
{
|
||||
return Inertia::render('admin/movies/List');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class MoviesPageController extends Controller
|
||||
{
|
||||
public function __invoke(): Response
|
||||
{
|
||||
return Inertia::render('admin/movies/Index');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Modules\Movies\Http\Requests\SearchMoviesRequest;
|
||||
use App\Modules\Movies\Services\Contracts\MovieProvider;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class SearchMoviesController extends Controller
|
||||
{
|
||||
public function __invoke(SearchMoviesRequest $request, MovieProvider $provider): JsonResponse
|
||||
{
|
||||
$q = $request->validated('q');
|
||||
$page = (int) $request->validated('page');
|
||||
$result = $provider->search($q, $page);
|
||||
|
||||
return response()->json($result);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Modules\Movies\Http\Requests\UpdateMovieRequest;
|
||||
use App\Modules\Movies\Services\Contracts\UpdateMovieServiceInterface;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class UpdateMovieController extends Controller
|
||||
{
|
||||
public function update(int $movie, UpdateMovieRequest $request, UpdateMovieServiceInterface $service): JsonResponse
|
||||
{
|
||||
$data = $request->validated();
|
||||
$updated = $service->handle($movie, $data);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'ok',
|
||||
'movie' => $updated,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Http\Controllers\Browse\Entities;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Modules\Movies\Services\Contracts\GetActorWithMoviesServiceInterface;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ActorShowController extends Controller
|
||||
{
|
||||
public function __invoke(string $actor, Request $request, GetActorWithMoviesServiceInterface $service): JsonResponse
|
||||
{
|
||||
$id = (int) $actor;
|
||||
$result = $service->handle($id, [
|
||||
'q' => $request->string('q')->toString(),
|
||||
'per_page' => $request->integer('per_page', 20),
|
||||
'sort' => $request->string('sort')->toString() ?: 'title_asc',
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'entity' => $result['entity'],
|
||||
'movies' => $result['movies'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Http\Controllers\Browse\Entities;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Modules\Movies\Services\Contracts\GetDirectorWithMoviesServiceInterface;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DirectorShowController extends Controller
|
||||
{
|
||||
public function __invoke(string $director, Request $request, GetDirectorWithMoviesServiceInterface $service): JsonResponse
|
||||
{
|
||||
$id = (int) $director;
|
||||
$result = $service->handle($id, [
|
||||
'q' => $request->string('q')->toString(),
|
||||
'per_page' => $request->integer('per_page', 20),
|
||||
'sort' => $request->string('sort')->toString() ?: 'title_asc',
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'entity' => $result['entity'],
|
||||
'movies' => $result['movies'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Http\Controllers\Browse\Entities;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Modules\Movies\Services\Contracts\GetGenreWithMoviesServiceInterface;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class GenreShowController extends Controller
|
||||
{
|
||||
public function __invoke(string $genre, Request $request, GetGenreWithMoviesServiceInterface $service): JsonResponse
|
||||
{
|
||||
$id = (int) $genre;
|
||||
$result = $service->handle($id, [
|
||||
'q' => $request->string('q')->toString(),
|
||||
'per_page' => $request->integer('per_page', 20),
|
||||
'sort' => $request->string('sort')->toString() ?: 'title_asc',
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'entity' => $result['entity'],
|
||||
'movies' => $result['movies'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Http\Controllers\Browse\Entities;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Modules\Movies\Services\Contracts\GetStudioWithMoviesServiceInterface;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class StudioShowController extends Controller
|
||||
{
|
||||
public function __invoke(string $studio, Request $request, GetStudioWithMoviesServiceInterface $service): JsonResponse
|
||||
{
|
||||
$id = (int) $studio;
|
||||
$result = $service->handle($id, [
|
||||
'q' => $request->string('q')->toString(),
|
||||
'per_page' => $request->integer('per_page', 20),
|
||||
'sort' => $request->string('sort')->toString() ?: 'title_asc',
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'entity' => $result['entity'],
|
||||
'movies' => $result['movies'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Http\Controllers\Browse;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Modules\Movies\Services\Contracts\ShowMovieServiceInterface;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class MovieShowController extends Controller
|
||||
{
|
||||
public function __invoke(string $movie, ShowMovieServiceInterface $service): JsonResponse
|
||||
{
|
||||
$id = (int) $movie;
|
||||
$model = $service->getById($id);
|
||||
return response()->json($model);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Http\Controllers\Browse;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Modules\Movies\Services\Contracts\ListMoviesServiceInterface;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class MoviesIndexController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request, ListMoviesServiceInterface $service): JsonResponse
|
||||
{
|
||||
$paginator = $service->list([
|
||||
'q' => $request->string('q')->toString(),
|
||||
'per_page' => $request->integer('per_page', 20),
|
||||
'sort' => $request->string('sort')->toString() ?: 'title_asc',
|
||||
'genre' => $request->string('genre')->toString(),
|
||||
'rating' => $request->string('rating')->toString(),
|
||||
'year_min' => $request->has('year_min') ? $request->integer('year_min') : null,
|
||||
'year_max' => $request->has('year_max') ? $request->integer('year_max') : null,
|
||||
'actor' => $request->string('actor')->toString(),
|
||||
'director' => $request->string('director')->toString(),
|
||||
'studio' => $request->string('studio')->toString(),
|
||||
]);
|
||||
|
||||
return response()->json($paginator);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Http\Controllers\Pages;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class ActorsShowPageController extends Controller
|
||||
{
|
||||
public function __invoke(string $actor): Response
|
||||
{
|
||||
return Inertia::render('actors/Show', [
|
||||
'entityId' => (int) $actor,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Http\Controllers\Pages;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class DirectorsShowPageController extends Controller
|
||||
{
|
||||
public function __invoke(string $director): Response
|
||||
{
|
||||
return Inertia::render('directors/Show', [
|
||||
'entityId' => (int) $director,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Http\Controllers\Pages;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class GenresShowPageController extends Controller
|
||||
{
|
||||
public function __invoke(string $genre): Response
|
||||
{
|
||||
return Inertia::render('genres/Show', [
|
||||
'entityId' => (int) $genre,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Http\Controllers\Pages;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class PublicMovieShowPageController extends Controller
|
||||
{
|
||||
public function __invoke(string $movie): Response
|
||||
{
|
||||
return Inertia::render('movies/Show', [
|
||||
'movieId' => (int) $movie,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Http\Controllers\Pages;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class PublicMoviesPageController extends Controller
|
||||
{
|
||||
public function __invoke(): Response
|
||||
{
|
||||
return Inertia::render('movies/Index');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Http\Controllers\Pages;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class StudiosShowPageController extends Controller
|
||||
{
|
||||
public function __invoke(string $studio): Response
|
||||
{
|
||||
return Inertia::render('studios/Show', [
|
||||
'entityId' => (int) $studio,
|
||||
]);
|
||||
}
|
||||
}
|
||||
22
app/Modules/Movies/Http/Requests/AcceptMovieRequest.php
Normal file
22
app/Modules/Movies/Http/Requests/AcceptMovieRequest.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class AcceptMovieRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return (bool) $this->user();
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'provider_id' => ['required', 'string'],
|
||||
'mode' => ['required', Rule::in(['overwrite', 'duplicate'])],
|
||||
];
|
||||
}
|
||||
}
|
||||
18
app/Modules/Movies/Http/Requests/DestroyMovieRequest.php
Normal file
18
app/Modules/Movies/Http/Requests/DestroyMovieRequest.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class DestroyMovieRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return (bool) $this->user();
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
21
app/Modules/Movies/Http/Requests/ExistsMovieRequest.php
Normal file
21
app/Modules/Movies/Http/Requests/ExistsMovieRequest.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ExistsMovieRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return (bool) $this->user();
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'provider' => ['sometimes', 'string'],
|
||||
'provider_id' => ['required', 'string'],
|
||||
];
|
||||
}
|
||||
}
|
||||
28
app/Modules/Movies/Http/Requests/SearchMoviesRequest.php
Normal file
28
app/Modules/Movies/Http/Requests/SearchMoviesRequest.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class SearchMoviesRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return (bool) $this->user();
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'q' => ['required', 'string', 'min:2'],
|
||||
'page' => ['sometimes', 'integer', 'min:1'],
|
||||
];
|
||||
}
|
||||
|
||||
public function validated($key = null, $default = null)
|
||||
{
|
||||
$data = parent::validated();
|
||||
$data['page'] = isset($data['page']) ? (int) $data['page'] : 1;
|
||||
return $key ? ($data[$key] ?? $default) : $data;
|
||||
}
|
||||
}
|
||||
44
app/Modules/Movies/Http/Requests/UpdateMovieRequest.php
Normal file
44
app/Modules/Movies/Http/Requests/UpdateMovieRequest.php
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateMovieRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
// Any authenticated user can manage
|
||||
return (bool) $this->user();
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
// Scalars (all optional)
|
||||
'title' => ['sometimes', 'string', 'min:1'],
|
||||
'original_title' => ['sometimes', 'nullable', 'string'],
|
||||
'description' => ['sometimes', 'nullable', 'string'],
|
||||
'poster_url' => ['sometimes', 'nullable', 'url'],
|
||||
'backdrop_url' => ['sometimes', 'nullable', 'url'],
|
||||
'rating' => ['sometimes', 'nullable', 'string', 'max:20'],
|
||||
'release_date' => ['sometimes', 'nullable', 'date'],
|
||||
'year' => ['sometimes', 'nullable', 'integer', 'min:1800', 'max:3000'],
|
||||
'runtime' => ['sometimes', 'nullable', 'integer', 'min:1', 'max:10000'],
|
||||
|
||||
// Relations via free-text chips (arrays of strings)
|
||||
'genres' => ['sometimes', 'array'],
|
||||
'genres.*' => ['string', 'min:1'],
|
||||
'actors' => ['sometimes', 'array'],
|
||||
'actors.*' => ['string', 'min:1'],
|
||||
'directors' => ['sometimes', 'array'],
|
||||
'directors.*' => ['string', 'min:1'],
|
||||
'studios' => ['sometimes', 'array'],
|
||||
'studios.*' => ['string', 'min:1'],
|
||||
'countries' => ['sometimes', 'array'],
|
||||
'countries.*' => ['string', 'min:1'],
|
||||
'languages' => ['sometimes', 'array'],
|
||||
'languages.*' => ['string', 'min:1'],
|
||||
];
|
||||
}
|
||||
}
|
||||
22
app/Modules/Movies/Models/Actor.php
Normal file
22
app/Modules/Movies/Models/Actor.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class Actor extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
protected $table = 'actors';
|
||||
protected $fillable = ['name'];
|
||||
|
||||
public function movies(): BelongsToMany { return $this->belongsToMany(Movie::class, 'movie_actor'); }
|
||||
|
||||
protected static function newFactory(): Factory
|
||||
{
|
||||
return \Database\Factories\ActorFactory::new();
|
||||
}
|
||||
}
|
||||
22
app/Modules/Movies/Models/Country.php
Normal file
22
app/Modules/Movies/Models/Country.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class Country extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
protected $table = 'countries';
|
||||
protected $fillable = ['name'];
|
||||
|
||||
public function movies(): BelongsToMany { return $this->belongsToMany(Movie::class, 'movie_country'); }
|
||||
|
||||
protected static function newFactory(): Factory
|
||||
{
|
||||
return \Database\Factories\CountryFactory::new();
|
||||
}
|
||||
}
|
||||
22
app/Modules/Movies/Models/Director.php
Normal file
22
app/Modules/Movies/Models/Director.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class Director extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
protected $table = 'directors';
|
||||
protected $fillable = ['name'];
|
||||
|
||||
public function movies(): BelongsToMany { return $this->belongsToMany(Movie::class, 'movie_director'); }
|
||||
|
||||
protected static function newFactory(): Factory
|
||||
{
|
||||
return \Database\Factories\DirectorFactory::new();
|
||||
}
|
||||
}
|
||||
22
app/Modules/Movies/Models/Genre.php
Normal file
22
app/Modules/Movies/Models/Genre.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class Genre extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
protected $table = 'genres';
|
||||
protected $fillable = ['name'];
|
||||
|
||||
public function movies(): BelongsToMany { return $this->belongsToMany(Movie::class, 'movie_genre'); }
|
||||
|
||||
protected static function newFactory(): Factory
|
||||
{
|
||||
return \Database\Factories\GenreFactory::new();
|
||||
}
|
||||
}
|
||||
22
app/Modules/Movies/Models/Language.php
Normal file
22
app/Modules/Movies/Models/Language.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class Language extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
protected $table = 'languages';
|
||||
protected $fillable = ['name'];
|
||||
|
||||
public function movies(): BelongsToMany { return $this->belongsToMany(Movie::class, 'movie_language'); }
|
||||
|
||||
protected static function newFactory(): Factory
|
||||
{
|
||||
return \Database\Factories\LanguageFactory::new();
|
||||
}
|
||||
}
|
||||
46
app/Modules/Movies/Models/Movie.php
Normal file
46
app/Modules/Movies/Models/Movie.php
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class Movie extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
protected $table = 'movies';
|
||||
|
||||
protected $fillable = [
|
||||
'provider',
|
||||
'provider_id',
|
||||
'external_ids',
|
||||
'title',
|
||||
'original_title',
|
||||
'description',
|
||||
'poster_url',
|
||||
'backdrop_url',
|
||||
'rating',
|
||||
'release_date',
|
||||
'year',
|
||||
'runtime',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'external_ids' => 'array',
|
||||
'release_date' => 'date',
|
||||
];
|
||||
|
||||
public function genres(): BelongsToMany { return $this->belongsToMany(Genre::class, 'movie_genre'); }
|
||||
public function actors(): BelongsToMany { return $this->belongsToMany(Actor::class, 'movie_actor'); }
|
||||
public function directors(): BelongsToMany { return $this->belongsToMany(Director::class, 'movie_director'); }
|
||||
public function studios(): BelongsToMany { return $this->belongsToMany(Studio::class, 'movie_studio'); }
|
||||
public function countries(): BelongsToMany { return $this->belongsToMany(Country::class, 'movie_country'); }
|
||||
public function languages(): BelongsToMany { return $this->belongsToMany(Language::class, 'movie_language'); }
|
||||
|
||||
protected static function newFactory(): Factory
|
||||
{
|
||||
return \Database\Factories\MovieFactory::new();
|
||||
}
|
||||
}
|
||||
22
app/Modules/Movies/Models/Studio.php
Normal file
22
app/Modules/Movies/Models/Studio.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class Studio extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
protected $table = 'studios';
|
||||
protected $fillable = ['name'];
|
||||
|
||||
public function movies(): BelongsToMany { return $this->belongsToMany(Movie::class, 'movie_studio'); }
|
||||
|
||||
protected static function newFactory(): Factory
|
||||
{
|
||||
return \Database\Factories\StudioFactory::new();
|
||||
}
|
||||
}
|
||||
93
app/Modules/Movies/Providers/MoviesServiceProvider.php
Normal file
93
app/Modules/Movies/Providers/MoviesServiceProvider.php
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Modules\Movies\Services\Contracts\MovieProvider;
|
||||
use App\Modules\Movies\Services\Omdb\OmdbMovieProvider;
|
||||
use App\Modules\Movies\Services\Contracts\ListMoviesServiceInterface;
|
||||
use App\Modules\Movies\Services\Contracts\ShowMovieServiceInterface;
|
||||
use App\Modules\Movies\Services\Contracts\UpsertMovieServiceInterface;
|
||||
use App\Modules\Movies\Services\Contracts\CheckMovieExistsServiceInterface;
|
||||
use App\Modules\Movies\Services\Contracts\{GetActorWithMoviesServiceInterface, GetDirectorWithMoviesServiceInterface, GetStudioWithMoviesServiceInterface, GetGenreWithMoviesServiceInterface};
|
||||
use App\Modules\Movies\Services\Contracts\{UpdateMovieServiceInterface, DeleteMovieServiceInterface};
|
||||
use App\Modules\Movies\Services\Browse\ListMoviesService;
|
||||
use App\Modules\Movies\Services\Browse\ShowMovieService;
|
||||
use App\Modules\Movies\Services\UpsertMovieFromProvider;
|
||||
use App\Modules\Movies\Services\CheckMovieExistsService;
|
||||
use App\Modules\Movies\Services\Browse\Entities\{GetActorWithMoviesService, GetDirectorWithMoviesService, GetStudioWithMoviesService, GetGenreWithMoviesService};
|
||||
use App\Modules\Movies\Services\{UpdateMovieService, DeleteMovieService};
|
||||
|
||||
class MoviesServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
// Bind movie provider based on config
|
||||
$this->app->bind(MovieProvider::class, function () {
|
||||
$provider = config('movies.provider', 'omdb');
|
||||
return match ($provider) {
|
||||
'omdb' => new OmdbMovieProvider(),
|
||||
default => new OmdbMovieProvider(),
|
||||
};
|
||||
});
|
||||
|
||||
// Bind service interfaces to implementations
|
||||
$this->app->bind(ListMoviesServiceInterface::class, ListMoviesService::class);
|
||||
$this->app->bind(ShowMovieServiceInterface::class, ShowMovieService::class);
|
||||
$this->app->bind(UpsertMovieServiceInterface::class, UpsertMovieFromProvider::class);
|
||||
$this->app->bind(CheckMovieExistsServiceInterface::class, CheckMovieExistsService::class);
|
||||
|
||||
// Entity services
|
||||
$this->app->bind(GetActorWithMoviesServiceInterface::class, GetActorWithMoviesService::class);
|
||||
$this->app->bind(GetDirectorWithMoviesServiceInterface::class, GetDirectorWithMoviesService::class);
|
||||
$this->app->bind(GetStudioWithMoviesServiceInterface::class, GetStudioWithMoviesService::class);
|
||||
$this->app->bind(GetGenreWithMoviesServiceInterface::class, GetGenreWithMoviesService::class);
|
||||
|
||||
// Admin management services
|
||||
$this->app->bind(UpdateMovieServiceInterface::class, UpdateMovieService::class);
|
||||
$this->app->bind(DeleteMovieServiceInterface::class, DeleteMovieService::class);
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
// Module routes
|
||||
Route::middleware(['web', 'auth', 'verified'])
|
||||
->prefix('admin/movies')
|
||||
->as('admin.movies.')
|
||||
->group(__DIR__ . '/../routes/admin.php');
|
||||
|
||||
// Public API (JSON) routes
|
||||
Route::middleware(['web'])
|
||||
->prefix('api/movies')
|
||||
->as('api.movies.')
|
||||
->group(__DIR__ . '/../routes/api.php');
|
||||
|
||||
// Entity API routes (actors/directors/studios/genres)
|
||||
Route::middleware(['web'])
|
||||
->prefix('api')
|
||||
->as('api.movies.entities.')
|
||||
->group(function () {
|
||||
Route::prefix('actors')->as('actors.')->group(__DIR__ . '/../routes/api_actors.php');
|
||||
Route::prefix('directors')->as('directors.')->group(__DIR__ . '/../routes/api_directors.php');
|
||||
Route::prefix('studios')->as('studios.')->group(__DIR__ . '/../routes/api_studios.php');
|
||||
Route::prefix('genres')->as('genres.')->group(__DIR__ . '/../routes/api_genres.php');
|
||||
});
|
||||
|
||||
// Public Inertia pages
|
||||
Route::middleware(['web'])
|
||||
->prefix('movies')
|
||||
->as('movies.')
|
||||
->group(__DIR__ . '/../routes/pages.php');
|
||||
|
||||
// Entity Inertia pages
|
||||
Route::middleware(['web'])
|
||||
->as('entities.pages.')
|
||||
->group(function () {
|
||||
Route::prefix('actors')->as('actors.')->group(__DIR__ . '/../routes/pages_actors.php');
|
||||
Route::prefix('directors')->as('directors.')->group(__DIR__ . '/../routes/pages_directors.php');
|
||||
Route::prefix('studios')->as('studios.')->group(__DIR__ . '/../routes/pages_studios.php');
|
||||
Route::prefix('genres')->as('genres.')->group(__DIR__ . '/../routes/pages_genres.php');
|
||||
});
|
||||
}
|
||||
}
|
||||
60
app/Modules/Movies/README.md
Normal file
60
app/Modules/Movies/README.md
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# Movies Module
|
||||
|
||||
This module implements service-first Movies functionality, including:
|
||||
- Admin search + accept flow (OMDb)
|
||||
- Public browsing (Inertia pages) backed by JSON API
|
||||
- Strict controller→service separation
|
||||
|
||||
## Endpoints
|
||||
|
||||
Admin (auth required)
|
||||
- GET /admin/movies → Inertia admin page (search + accept)
|
||||
- GET /admin/movies/search?q=&page= → JSON results from OMDb (10 per page)
|
||||
- GET /admin/movies/exists?provider_id= → Duplicate check
|
||||
- POST /admin/movies/accept { provider_id, mode: overwrite|duplicate } → Persist movie
|
||||
|
||||
Public
|
||||
- Inertia pages: GET /movies, GET /movies/{id}
|
||||
- JSON API: GET /api/movies, GET /api/movies/{id}
|
||||
|
||||
## Data model
|
||||
- Fully normalized tables: movies, genres, actors, directors, studios, countries, languages + pivot tables.
|
||||
- Provider ID is not unique. Duplicates are allowed when “Save as Duplicate” is selected.
|
||||
- Images are stored as remote URLs only (not downloaded).
|
||||
|
||||
## Services & Contracts
|
||||
- Provider: App\\Modules\\Movies\\Services\\Contracts\\MovieProvider (OMDb implementation)
|
||||
- Browse: ListMoviesServiceInterface, ShowMovieServiceInterface
|
||||
- Admin: UpsertMovieServiceInterface, CheckMovieExistsServiceInterface
|
||||
|
||||
All DI bindings are registered in MoviesServiceProvider.
|
||||
|
||||
## Admin flow
|
||||
1. Search calls /admin/movies/search (OMDb search). List shows poster, title, year only (no details calls).
|
||||
2. Accept button:
|
||||
- If unique, accepts immediately (no modal).
|
||||
- If duplicate exists by provider_id, shows modal: Overwrite | Save as Duplicate | Cancel.
|
||||
3. On successful accept, a small toast appears and the row is marked “Accepted”.
|
||||
|
||||
## Public browsing
|
||||
- /movies (Inertia) fetches from /api/movies with infinite scroll. Each row shows poster, title, year, rating, genres, and a ~50-word description snippet.
|
||||
- /movies/{id} (Inertia) fetches from /api/movies/{id} and shows details with all relations.
|
||||
|
||||
## Environment
|
||||
Add to .env (values shown as placeholders):
|
||||
```
|
||||
OMDB_API_KEY=
|
||||
OMDB_LANGUAGE=en-US
|
||||
OMDB_CACHE_TTL=3600
|
||||
```
|
||||
|
||||
Set your real OMDb key locally (per user decision). Timezone is America/Chicago.
|
||||
|
||||
## Development
|
||||
- Run backend: `php artisan serve`
|
||||
- Frontend dev: `npm install && npm run dev`
|
||||
- Tests: `./vendor/bin/pest`
|
||||
|
||||
## Notes
|
||||
- Pagination: OMDb search uses 10/page; public DB pagination defaults to 20/page (configurable at request).
|
||||
- Controller classes contain no business logic; all logic resides in services.
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Services\Browse\Entities;
|
||||
|
||||
use App\Modules\Movies\Models\{Actor, Movie};
|
||||
use App\Modules\Movies\Services\Contracts\GetActorWithMoviesServiceInterface;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
|
||||
class GetActorWithMoviesService implements GetActorWithMoviesServiceInterface
|
||||
{
|
||||
/**
|
||||
* @param int $id
|
||||
* @param array{q?: string|null, per_page?: int|null} $params
|
||||
* @return array{entity: Actor, movies: LengthAwarePaginator}
|
||||
*/
|
||||
public function handle(int $id, array $params = []): array
|
||||
{
|
||||
$actor = Actor::query()->find($id);
|
||||
if (!$actor) {
|
||||
throw (new ModelNotFoundException())->setModel(Actor::class, [$id]);
|
||||
}
|
||||
|
||||
$perPage = max(1, min((int)($params['per_page'] ?? 20), 50));
|
||||
$q = isset($params['q']) ? trim((string)$params['q']) : '';
|
||||
|
||||
$movies = Movie::query()
|
||||
->whereHas('actors', fn($q2) => $q2->whereKey($actor->id))
|
||||
->with(['genres'])
|
||||
->when($q !== '', fn($query) => $query->where('title', 'like', "%{$q}%"))
|
||||
->latest()
|
||||
->paginate($perPage);
|
||||
|
||||
return [
|
||||
'entity' => $actor,
|
||||
'movies' => $movies,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Services\Browse\Entities;
|
||||
|
||||
use App\Modules\Movies\Models\{Director, Movie};
|
||||
use App\Modules\Movies\Services\Contracts\GetDirectorWithMoviesServiceInterface;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
|
||||
class GetDirectorWithMoviesService implements GetDirectorWithMoviesServiceInterface
|
||||
{
|
||||
/**
|
||||
* @param int $id
|
||||
* @param array{q?: string|null, per_page?: int|null} $params
|
||||
* @return array{entity: Director, movies: LengthAwarePaginator}
|
||||
*/
|
||||
public function handle(int $id, array $params = []): array
|
||||
{
|
||||
$director = Director::query()->find($id);
|
||||
if (!$director) {
|
||||
throw (new ModelNotFoundException())->setModel(Director::class, [$id]);
|
||||
}
|
||||
|
||||
$perPage = max(1, min((int)($params['per_page'] ?? 20), 50));
|
||||
$q = isset($params['q']) ? trim((string)$params['q']) : '';
|
||||
|
||||
$movies = Movie::query()
|
||||
->whereHas('directors', fn($q2) => $q2->whereKey($director->id))
|
||||
->with(['genres'])
|
||||
->when($q !== '', fn($query) => $query->where('title', 'like', "%{$q}%"))
|
||||
->latest()
|
||||
->paginate($perPage);
|
||||
|
||||
return [
|
||||
'entity' => $director,
|
||||
'movies' => $movies,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Services\Browse\Entities;
|
||||
|
||||
use App\Modules\Movies\Models\{Genre, Movie};
|
||||
use App\Modules\Movies\Services\Contracts\GetGenreWithMoviesServiceInterface;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
|
||||
class GetGenreWithMoviesService implements GetGenreWithMoviesServiceInterface
|
||||
{
|
||||
/**
|
||||
* @param int $id
|
||||
* @param array{q?: string|null, per_page?: int|null} $params
|
||||
* @return array{entity: Genre, movies: LengthAwarePaginator}
|
||||
*/
|
||||
public function handle(int $id, array $params = []): array
|
||||
{
|
||||
$genre = Genre::query()->find($id);
|
||||
if (!$genre) {
|
||||
throw (new ModelNotFoundException())->setModel(Genre::class, [$id]);
|
||||
}
|
||||
|
||||
$perPage = max(1, min((int)($params['per_page'] ?? 20), 50));
|
||||
$q = isset($params['q']) ? trim((string)$params['q']) : '';
|
||||
$sort = isset($params['sort']) ? trim((string)$params['sort']) : 'title_asc';
|
||||
|
||||
$moviesQuery = Movie::query()
|
||||
->whereHas('genres', fn($q2) => $q2->whereKey($genre->id))
|
||||
->with(['genres'])
|
||||
->when($q !== '', fn($query) => $query->where('title', 'like', "%{$q}%"));
|
||||
|
||||
switch ($sort) {
|
||||
case 'title_desc':
|
||||
$moviesQuery->orderBy('title', 'desc');
|
||||
break;
|
||||
case 'newest':
|
||||
$moviesQuery->orderBy('created_at', 'desc');
|
||||
break;
|
||||
case 'oldest':
|
||||
$moviesQuery->orderBy('created_at', 'asc');
|
||||
break;
|
||||
case 'year_asc':
|
||||
$moviesQuery->orderBy('year', 'asc');
|
||||
break;
|
||||
case 'year_desc':
|
||||
$moviesQuery->orderBy('year', 'desc');
|
||||
break;
|
||||
case 'title_asc':
|
||||
default:
|
||||
$moviesQuery->orderBy('title', 'asc');
|
||||
break;
|
||||
}
|
||||
|
||||
$movies = $moviesQuery->paginate($perPage);
|
||||
|
||||
return [
|
||||
'entity' => $genre,
|
||||
'movies' => $movies,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Services\Browse\Entities;
|
||||
|
||||
use App\Modules\Movies\Models\{Studio, Movie};
|
||||
use App\Modules\Movies\Services\Contracts\GetStudioWithMoviesServiceInterface;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
|
||||
class GetStudioWithMoviesService implements GetStudioWithMoviesServiceInterface
|
||||
{
|
||||
/**
|
||||
* @param int $id
|
||||
* @param array{q?: string|null, per_page?: int|null} $params
|
||||
* @return array{entity: Studio, movies: LengthAwarePaginator}
|
||||
*/
|
||||
public function handle(int $id, array $params = []): array
|
||||
{
|
||||
$studio = Studio::query()->find($id);
|
||||
if (!$studio) {
|
||||
throw (new ModelNotFoundException())->setModel(Studio::class, [$id]);
|
||||
}
|
||||
|
||||
$perPage = max(1, min((int)($params['per_page'] ?? 20), 50));
|
||||
$q = isset($params['q']) ? trim((string)$params['q']) : '';
|
||||
$sort = isset($params['sort']) ? trim((string)$params['sort']) : 'title_asc';
|
||||
|
||||
$moviesQuery = Movie::query()
|
||||
->whereHas('studios', fn($q2) => $q2->whereKey($studio->id))
|
||||
->with(['genres'])
|
||||
->when($q !== '', fn($query) => $query->where('title', 'like', "%{$q}%"));
|
||||
|
||||
switch ($sort) {
|
||||
case 'title_desc':
|
||||
$moviesQuery->orderBy('title', 'desc');
|
||||
break;
|
||||
case 'newest':
|
||||
$moviesQuery->orderBy('created_at', 'desc');
|
||||
break;
|
||||
case 'oldest':
|
||||
$moviesQuery->orderBy('created_at', 'asc');
|
||||
break;
|
||||
case 'year_asc':
|
||||
$moviesQuery->orderBy('year', 'asc');
|
||||
break;
|
||||
case 'year_desc':
|
||||
$moviesQuery->orderBy('year', 'desc');
|
||||
break;
|
||||
case 'title_asc':
|
||||
default:
|
||||
$moviesQuery->orderBy('title', 'asc');
|
||||
break;
|
||||
}
|
||||
|
||||
$movies = $moviesQuery->paginate($perPage);
|
||||
|
||||
return [
|
||||
'entity' => $studio,
|
||||
'movies' => $movies,
|
||||
];
|
||||
}
|
||||
}
|
||||
98
app/Modules/Movies/Services/Browse/ListMoviesService.php
Normal file
98
app/Modules/Movies/Services/Browse/ListMoviesService.php
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Services\Browse;
|
||||
|
||||
use App\Modules\Movies\Models\Movie;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use App\Modules\Movies\Services\Contracts\ListMoviesServiceInterface;
|
||||
|
||||
class ListMoviesService implements ListMoviesServiceInterface
|
||||
{
|
||||
/**
|
||||
* Return a paginated list of movies with optional search, filters, and sort.
|
||||
*
|
||||
* @param array{
|
||||
* q?: string|null,
|
||||
* per_page?: int|null,
|
||||
* sort?: string|null,
|
||||
* genre?: string|null,
|
||||
* rating?: string|null,
|
||||
* year_min?: int|null,
|
||||
* year_max?: int|null,
|
||||
* actor?: string|null,
|
||||
* director?: string|null,
|
||||
* studio?: string|null,
|
||||
* } $params
|
||||
*/
|
||||
public function list(array $params = []): LengthAwarePaginator
|
||||
{
|
||||
$perPage = (int) ($params['per_page'] ?? 20);
|
||||
$q = isset($params['q']) ? trim((string) $params['q']) : '';
|
||||
$sort = isset($params['sort']) ? trim((string) $params['sort']) : 'title_asc';
|
||||
|
||||
$genre = isset($params['genre']) ? trim((string) $params['genre']) : '';
|
||||
$rating = isset($params['rating']) ? trim((string) $params['rating']) : '';
|
||||
$yearMin = isset($params['year_min']) ? (int) $params['year_min'] : null;
|
||||
$yearMax = isset($params['year_max']) ? (int) $params['year_max'] : null;
|
||||
$actor = isset($params['actor']) ? trim((string) $params['actor']) : '';
|
||||
$director = isset($params['director']) ? trim((string) $params['director']) : '';
|
||||
$studio = isset($params['studio']) ? trim((string) $params['studio']) : '';
|
||||
|
||||
$query = Movie::query()->with(['genres', 'actors', 'directors']);
|
||||
|
||||
if ($q !== '') {
|
||||
$query->where('title', 'like', "%{$q}%");
|
||||
}
|
||||
|
||||
if ($genre !== '') {
|
||||
$ln = mb_strtolower($genre);
|
||||
$query->whereHas('genres', fn($q2) => $q2->whereRaw('lower(name) = ?', [$ln]));
|
||||
}
|
||||
if ($rating !== '') {
|
||||
$query->where('rating', $rating);
|
||||
}
|
||||
if ($yearMin !== null) {
|
||||
$query->where('year', '>=', $yearMin);
|
||||
}
|
||||
if ($yearMax !== null) {
|
||||
$query->where('year', '<=', $yearMax);
|
||||
}
|
||||
if ($actor !== '') {
|
||||
$ln = mb_strtolower($actor);
|
||||
$query->whereHas('actors', fn($q2) => $q2->whereRaw('lower(name) = ?', [$ln]));
|
||||
}
|
||||
if ($director !== '') {
|
||||
$ln = mb_strtolower($director);
|
||||
$query->whereHas('directors', fn($q2) => $q2->whereRaw('lower(name) = ?', [$ln]));
|
||||
}
|
||||
if ($studio !== '') {
|
||||
$ln = mb_strtolower($studio);
|
||||
$query->whereHas('studios', fn($q2) => $q2->whereRaw('lower(name) = ?', [$ln]));
|
||||
}
|
||||
|
||||
// Sorting
|
||||
switch ($sort) {
|
||||
case 'title_desc':
|
||||
$query->orderBy('title', 'desc');
|
||||
break;
|
||||
case 'newest':
|
||||
$query->orderBy('created_at', 'desc');
|
||||
break;
|
||||
case 'oldest':
|
||||
$query->orderBy('created_at', 'asc');
|
||||
break;
|
||||
case 'year_asc':
|
||||
$query->orderBy('year', 'asc');
|
||||
break;
|
||||
case 'year_desc':
|
||||
$query->orderBy('year', 'desc');
|
||||
break;
|
||||
case 'title_asc':
|
||||
default:
|
||||
$query->orderBy('title', 'asc');
|
||||
break;
|
||||
}
|
||||
|
||||
return $query->paginate($perPage);
|
||||
}
|
||||
}
|
||||
26
app/Modules/Movies/Services/Browse/ShowMovieService.php
Normal file
26
app/Modules/Movies/Services/Browse/ShowMovieService.php
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Services\Browse;
|
||||
|
||||
use App\Modules\Movies\Models\Movie;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use App\Modules\Movies\Services\Contracts\ShowMovieServiceInterface;
|
||||
|
||||
class ShowMovieService implements ShowMovieServiceInterface
|
||||
{
|
||||
/**
|
||||
* Fetch a single movie with its relations by ID.
|
||||
*/
|
||||
public function getById(int $id): Movie
|
||||
{
|
||||
$movie = Movie::query()
|
||||
->with(['genres', 'actors', 'directors', 'studios', 'countries', 'languages'])
|
||||
->find($id);
|
||||
|
||||
if (!$movie) {
|
||||
throw (new ModelNotFoundException())->setModel(Movie::class, [$id]);
|
||||
}
|
||||
|
||||
return $movie;
|
||||
}
|
||||
}
|
||||
17
app/Modules/Movies/Services/CheckMovieExistsService.php
Normal file
17
app/Modules/Movies/Services/CheckMovieExistsService.php
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Services;
|
||||
|
||||
use App\Modules\Movies\Models\Movie;
|
||||
use App\Modules\Movies\Services\Contracts\CheckMovieExistsServiceInterface;
|
||||
|
||||
class CheckMovieExistsService implements CheckMovieExistsServiceInterface
|
||||
{
|
||||
public function findByProviderId(string $provider, string $providerId): ?Movie
|
||||
{
|
||||
return Movie::query()
|
||||
->where('provider', $provider)
|
||||
->where('provider_id', $providerId)
|
||||
->first();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Services\Contracts;
|
||||
|
||||
use App\Modules\Movies\Models\Movie;
|
||||
|
||||
interface CheckMovieExistsServiceInterface
|
||||
{
|
||||
public function findByProviderId(string $provider, string $providerId): ?Movie;
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Services\Contracts;
|
||||
|
||||
interface DeleteMovieServiceInterface
|
||||
{
|
||||
public function handle(int $id): void;
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Services\Contracts;
|
||||
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use App\Modules\Movies\Models\Actor;
|
||||
|
||||
interface GetActorWithMoviesServiceInterface
|
||||
{
|
||||
/**
|
||||
* @param int $id Actor ID
|
||||
* @param array{q?: string|null, per_page?: int|null} $params
|
||||
* @return array{entity: Actor, movies: LengthAwarePaginator}
|
||||
*/
|
||||
public function handle(int $id, array $params = []): array;
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Services\Contracts;
|
||||
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use App\Modules\Movies\Models\Director;
|
||||
|
||||
interface GetDirectorWithMoviesServiceInterface
|
||||
{
|
||||
/**
|
||||
* @param int $id Director ID
|
||||
* @param array{q?: string|null, per_page?: int|null} $params
|
||||
* @return array{entity: Director, movies: LengthAwarePaginator}
|
||||
*/
|
||||
public function handle(int $id, array $params = []): array;
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Services\Contracts;
|
||||
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use App\Modules\Movies\Models\Genre;
|
||||
|
||||
interface GetGenreWithMoviesServiceInterface
|
||||
{
|
||||
/**
|
||||
* @param int $id Genre ID
|
||||
* @param array{q?: string|null, per_page?: int|null} $params
|
||||
* @return array{entity: Genre, movies: LengthAwarePaginator}
|
||||
*/
|
||||
public function handle(int $id, array $params = []): array;
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Services\Contracts;
|
||||
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use App\Modules\Movies\Models\Studio;
|
||||
|
||||
interface GetStudioWithMoviesServiceInterface
|
||||
{
|
||||
/**
|
||||
* @param int $id Studio ID
|
||||
* @param array{q?: string|null, per_page?: int|null} $params
|
||||
* @return array{entity: Studio, movies: LengthAwarePaginator}
|
||||
*/
|
||||
public function handle(int $id, array $params = []): array;
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Services\Contracts;
|
||||
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
|
||||
interface ListMoviesServiceInterface
|
||||
{
|
||||
/**
|
||||
* @param array{q?: string|null, per_page?: int|null} $params
|
||||
*/
|
||||
public function list(array $params = []): LengthAwarePaginator;
|
||||
}
|
||||
25
app/Modules/Movies/Services/Contracts/MovieProvider.php
Normal file
25
app/Modules/Movies/Services/Contracts/MovieProvider.php
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Services\Contracts;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
interface MovieProvider
|
||||
{
|
||||
/**
|
||||
* Search movies by title.
|
||||
*
|
||||
* @param string $query
|
||||
* @param int $page 1-based page number
|
||||
* @return array{results: Collection, total: int, page: int, has_more: bool}
|
||||
*/
|
||||
public function search(string $query, int $page = 1): array;
|
||||
|
||||
/**
|
||||
* Fetch full movie details by provider-specific ID.
|
||||
*
|
||||
* @param string $id
|
||||
* @return array Movie details normalized to our internal structure
|
||||
*/
|
||||
public function details(string $id): array;
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Services\Contracts;
|
||||
|
||||
use App\Modules\Movies\Models\Movie;
|
||||
|
||||
interface ShowMovieServiceInterface
|
||||
{
|
||||
public function getById(int $id): Movie;
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Services\Contracts;
|
||||
|
||||
use App\Modules\Movies\Models\Movie;
|
||||
|
||||
interface UpdateMovieServiceInterface
|
||||
{
|
||||
/**
|
||||
* Update a movie's scalar fields and sync relations.
|
||||
*
|
||||
* @param int $id Movie ID
|
||||
* @param array $data Accepts scalar fields (title, original_title, description, poster_url, backdrop_url, rating, release_date, year, runtime)
|
||||
* and relation name arrays: genres, actors, directors, studios, countries, languages (each array of names or IDs)
|
||||
*/
|
||||
public function handle(int $id, array $data): Movie;
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Services\Contracts;
|
||||
|
||||
use App\Modules\Movies\Models\Movie;
|
||||
|
||||
interface UpsertMovieServiceInterface
|
||||
{
|
||||
/**
|
||||
* Persist a movie and all relations from normalized provider details.
|
||||
*
|
||||
* @param array $details Output of MovieProvider::details
|
||||
* @param string $mode 'overwrite'|'duplicate'
|
||||
* @return Movie
|
||||
*/
|
||||
public function handle(array $details, string $mode = 'overwrite'): Movie;
|
||||
}
|
||||
22
app/Modules/Movies/Services/DeleteMovieService.php
Normal file
22
app/Modules/Movies/Services/DeleteMovieService.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Services;
|
||||
|
||||
use App\Modules\Movies\Models\Movie;
|
||||
use App\Modules\Movies\Services\Contracts\DeleteMovieServiceInterface;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
|
||||
class DeleteMovieService implements DeleteMovieServiceInterface
|
||||
{
|
||||
public function handle(int $id): void
|
||||
{
|
||||
/** @var Movie|null $movie */
|
||||
$movie = Movie::query()->find($id);
|
||||
if (!$movie) {
|
||||
throw (new ModelNotFoundException())->setModel(Movie::class, [$id]);
|
||||
}
|
||||
|
||||
// Hard delete cascades through pivots due to FK cascade in migrations
|
||||
$movie->delete();
|
||||
}
|
||||
}
|
||||
161
app/Modules/Movies/Services/Omdb/OmdbMovieProvider.php
Normal file
161
app/Modules/Movies/Services/Omdb/OmdbMovieProvider.php
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Services\Omdb;
|
||||
|
||||
use App\Modules\Movies\Services\Contracts\MovieProvider;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class OmdbMovieProvider implements MovieProvider
|
||||
{
|
||||
protected string $baseUrl;
|
||||
protected ?string $apiKey;
|
||||
protected int $cacheTtl;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->baseUrl = rtrim(config('movies.omdb.base_url', 'https://www.omdbapi.com/'), '/');
|
||||
$this->apiKey = config('movies.omdb.api_key');
|
||||
$this->cacheTtl = (int) config('movies.omdb.cache_ttl', 3600);
|
||||
}
|
||||
|
||||
public function search(string $query, int $page = 1): array
|
||||
{
|
||||
$query = trim($query);
|
||||
if ($query === '') {
|
||||
return ['results' => collect(), 'total' => 0, 'page' => 1, 'has_more' => false];
|
||||
}
|
||||
|
||||
$cacheKey = sprintf('omdb.search.%s.p%s', md5($query), $page);
|
||||
$data = Cache::remember($cacheKey, $this->cacheTtl, function () use ($query, $page) {
|
||||
$response = Http::acceptJson()
|
||||
->timeout(6)
|
||||
->retry(3, 300)
|
||||
->get($this->baseUrl, [
|
||||
'apikey' => $this->apiKey,
|
||||
's' => $query,
|
||||
'type' => 'movie',
|
||||
'page' => $page,
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
abort(502, 'Couldn’t reach OMDb. Please try again.');
|
||||
}
|
||||
|
||||
return $response->json();
|
||||
});
|
||||
|
||||
$results = collect();
|
||||
$total = 0;
|
||||
$hasMore = false;
|
||||
if (isset($data['Response']) && $data['Response'] === 'True' && isset($data['Search'])) {
|
||||
$total = (int) ($data['totalResults'] ?? 0);
|
||||
$hasMore = $page * 10 < $total; // OMDb pages of 10
|
||||
$results = collect($data['Search'])->map(function ($item) {
|
||||
return [
|
||||
'provider' => 'omdb',
|
||||
'provider_id' => $item['imdbID'] ?? null,
|
||||
'title' => $item['Title'] ?? null,
|
||||
'year' => $item['Year'] ?? null,
|
||||
'poster_url' => ($item['Poster'] ?? 'N/A') !== 'N/A' ? $item['Poster'] : null,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
return [
|
||||
'results' => $results,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'has_more' => $hasMore,
|
||||
];
|
||||
}
|
||||
|
||||
public function details(string $id): array
|
||||
{
|
||||
$id = trim($id);
|
||||
$cacheKey = sprintf('omdb.details.%s', $id);
|
||||
$data = Cache::remember($cacheKey, $this->cacheTtl, function () use ($id) {
|
||||
$response = Http::acceptJson()
|
||||
->timeout(6)
|
||||
->retry(3, 300)
|
||||
->get($this->baseUrl, [
|
||||
'apikey' => $this->apiKey,
|
||||
'i' => $id,
|
||||
'plot' => 'full',
|
||||
'type' => 'movie',
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
abort(502, 'Couldn’t reach OMDb. Please try again.');
|
||||
}
|
||||
|
||||
return $response->json();
|
||||
});
|
||||
|
||||
if (!isset($data['Response']) || $data['Response'] !== 'True') {
|
||||
// Map explicit OMDb errors that indicate provider-side issues differently if needed
|
||||
// Default to not found for details when OMDb says False
|
||||
abort(404, 'Movie not found in OMDb');
|
||||
}
|
||||
|
||||
// Normalize to internal structure
|
||||
$genres = $this->splitList($data['Genre'] ?? '');
|
||||
$actors = $this->splitList($data['Actors'] ?? '');
|
||||
$directors = $this->splitList($data['Director'] ?? '');
|
||||
$studios = $this->splitList($data['Production'] ?? '');
|
||||
$countries = $this->splitList($data['Country'] ?? '');
|
||||
$languages = $this->splitList($data['Language'] ?? '');
|
||||
|
||||
return [
|
||||
'provider' => 'omdb',
|
||||
'provider_id' => $data['imdbID'] ?? null,
|
||||
'external_ids' => [
|
||||
'imdb' => $data['imdbID'] ?? null,
|
||||
'omdb' => $data['imdbID'] ?? null,
|
||||
],
|
||||
'title' => $data['Title'] ?? null,
|
||||
'original_title' => $data['Title'] ?? null,
|
||||
'description' => $data['Plot'] ?? null,
|
||||
'poster_url' => ($data['Poster'] ?? 'N/A') !== 'N/A' ? $data['Poster'] : null,
|
||||
'backdrop_url' => null,
|
||||
'genres' => $genres,
|
||||
'rating' => $data['Rated'] ?? null,
|
||||
'release_date' => $this->parseDate($data['Released'] ?? null),
|
||||
'year' => isset($data['Year']) ? (int) substr($data['Year'], 0, 4) : null,
|
||||
'runtime' => $this->parseRuntime($data['Runtime'] ?? null),
|
||||
'actors' => $actors,
|
||||
'directors' => $directors,
|
||||
'studios' => $studios,
|
||||
'countries' => $countries,
|
||||
'languages' => $languages,
|
||||
];
|
||||
}
|
||||
|
||||
protected function splitList(?string $value): array
|
||||
{
|
||||
if (!$value || $value === 'N/A') return [];
|
||||
return collect(explode(',', $value))
|
||||
->map(fn($v) => trim($v))
|
||||
->filter()
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
protected function parseRuntime(?string $runtime): ?int
|
||||
{
|
||||
if (!$runtime) return null;
|
||||
if (preg_match('/(\d+) min/i', $runtime, $m)) {
|
||||
return (int) $m[1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function parseDate(?string $date): ?string
|
||||
{
|
||||
if (!$date || $date === 'N/A') return null;
|
||||
// OMDb uses formats like "01 Jan 2000"
|
||||
$ts = strtotime($date);
|
||||
return $ts ? date('Y-m-d', $ts) : null;
|
||||
}
|
||||
}
|
||||
68
app/Modules/Movies/Services/UpdateMovieService.php
Normal file
68
app/Modules/Movies/Services/UpdateMovieService.php
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Services;
|
||||
|
||||
use App\Modules\Movies\Models\{Movie, Genre, Actor, Director, Studio, Country, Language};
|
||||
use App\Modules\Movies\Services\Contracts\UpdateMovieServiceInterface;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class UpdateMovieService implements UpdateMovieServiceInterface
|
||||
{
|
||||
public function handle(int $id, array $data): Movie
|
||||
{
|
||||
/** @var Movie|null $movie */
|
||||
$movie = Movie::query()->find($id);
|
||||
if (!$movie) {
|
||||
throw (new ModelNotFoundException())->setModel(Movie::class, [$id]);
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($movie, $data) {
|
||||
// Scalars
|
||||
$movie->fill(array_intersect_key($data, array_flip([
|
||||
'title', 'original_title', 'description', 'poster_url', 'backdrop_url', 'rating', 'release_date', 'year', 'runtime',
|
||||
])));
|
||||
$movie->save();
|
||||
|
||||
// Relations via free-text chips (names)
|
||||
$this->syncNames($movie, Genre::class, 'genres', $data['genres'] ?? []);
|
||||
$this->syncNames($movie, Actor::class, 'actors', $data['actors'] ?? []);
|
||||
$this->syncNames($movie, Director::class, 'directors', $data['directors'] ?? []);
|
||||
$this->syncNames($movie, Studio::class, 'studios', $data['studios'] ?? []);
|
||||
$this->syncNames($movie, Country::class, 'countries', $data['countries'] ?? []);
|
||||
$this->syncNames($movie, Language::class, 'languages', $data['languages'] ?? []);
|
||||
|
||||
return $movie->refresh();
|
||||
});
|
||||
}
|
||||
|
||||
protected function syncNames(Movie $movie, string $modelClass, string $relation, array $names): void
|
||||
{
|
||||
if (!is_array($names)) return;
|
||||
$ids = collect($names)
|
||||
->filter()
|
||||
->map(fn($n) => $this->normalizeName((string)$n))
|
||||
->filter(fn($n) => $n !== '')
|
||||
->map(function ($normalized) use ($modelClass) {
|
||||
$existing = $modelClass::query()
|
||||
->whereRaw('lower(name) = ?', [mb_strtolower($normalized)])
|
||||
->first();
|
||||
if ($existing) return $existing->getKey();
|
||||
$m = new $modelClass();
|
||||
$m->setAttribute('name', $normalized);
|
||||
$m->save();
|
||||
return $m->getKey();
|
||||
})
|
||||
->values()
|
||||
->all();
|
||||
|
||||
if (method_exists($movie, $relation)) {
|
||||
$movie->{$relation}()->sync($ids);
|
||||
}
|
||||
}
|
||||
|
||||
protected function normalizeName(string $name): string
|
||||
{
|
||||
return trim(preg_replace('/\s+/u', ' ', $name) ?? '');
|
||||
}
|
||||
}
|
||||
118
app/Modules/Movies/Services/UpsertMovieFromProvider.php
Normal file
118
app/Modules/Movies/Services/UpsertMovieFromProvider.php
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
<?php
|
||||
|
||||
namespace App\Modules\Movies\Services;
|
||||
|
||||
use App\Modules\Movies\Models\{Movie, Genre, Actor, Director, Studio, Country, Language};
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Modules\Movies\Services\Contracts\UpsertMovieServiceInterface;
|
||||
|
||||
class UpsertMovieFromProvider implements UpsertMovieServiceInterface
|
||||
{
|
||||
/**
|
||||
* Persist a movie and all relations from normalized provider details.
|
||||
*
|
||||
* @param array $details Output of MovieProvider::details
|
||||
* @param string $mode 'overwrite'|'duplicate'
|
||||
* @return Movie
|
||||
*/
|
||||
public function handle(array $details, string $mode = 'overwrite'): Movie
|
||||
{
|
||||
return DB::transaction(function () use ($details, $mode) {
|
||||
$provider = $details['provider'] ?? null;
|
||||
$providerId = $details['provider_id'] ?? null;
|
||||
|
||||
$movie = null;
|
||||
if ($mode === 'overwrite' && $provider && $providerId) {
|
||||
$movie = Movie::query()
|
||||
->where('provider', $provider)
|
||||
->where('provider_id', $providerId)
|
||||
->first();
|
||||
}
|
||||
|
||||
if (!$movie) {
|
||||
$movie = new Movie();
|
||||
}
|
||||
|
||||
// Fill scalar fields
|
||||
$movie->fill([
|
||||
'provider' => $provider,
|
||||
'provider_id' => $providerId,
|
||||
'external_ids' => $details['external_ids'] ?? null,
|
||||
'title' => $details['title'] ?? '',
|
||||
'original_title' => $details['original_title'] ?? null,
|
||||
'description' => $details['description'] ?? null,
|
||||
'poster_url' => $details['poster_url'] ?? null,
|
||||
'backdrop_url' => $details['backdrop_url'] ?? null,
|
||||
'rating' => $details['rating'] ?? null,
|
||||
'release_date' => $details['release_date'] ?? null,
|
||||
'year' => $details['year'] ?? null,
|
||||
'runtime' => $details['runtime'] ?? null,
|
||||
]);
|
||||
|
||||
$movie->save();
|
||||
|
||||
// Sync relations using names from details payload
|
||||
$this->syncByNames($movie, Genre::class, 'genres', $details['genres'] ?? []);
|
||||
$this->syncByNames($movie, Actor::class, 'actors', $details['actors'] ?? []);
|
||||
$this->syncByNames($movie, Director::class, 'directors', $details['directors'] ?? []);
|
||||
$this->syncByNames($movie, Studio::class, 'studios', $details['studios'] ?? []);
|
||||
$this->syncByNames($movie, Country::class, 'countries', $details['countries'] ?? []);
|
||||
$this->syncByNames($movie, Language::class, 'languages', $details['languages'] ?? []);
|
||||
|
||||
return $movie->refresh();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Movie $movie
|
||||
* @param class-string $modelClass
|
||||
* @param string $relation
|
||||
* @param array $names Names to sync
|
||||
*/
|
||||
protected function syncByNames(Movie $movie, string $modelClass, string $relation, array $names): void
|
||||
{
|
||||
// Create or fetch IDs by name (case-insensitive normalization)
|
||||
$ids = collect($names)
|
||||
->filter()
|
||||
->map(function ($name) use ($modelClass) {
|
||||
$original = (string) $name;
|
||||
$normalized = $this->normalizeName($original);
|
||||
if ($normalized === '') return null;
|
||||
|
||||
// Find existing row in a case-insensitive way
|
||||
/** @var \Illuminate\Database\Eloquent\Model|null $existing */
|
||||
$existing = $modelClass::query()
|
||||
->whereRaw('lower(name) = ?', [mb_strtolower($normalized)])
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
return $existing->getKey();
|
||||
}
|
||||
|
||||
/** @var \Illuminate\Database\Eloquent\Model $model */
|
||||
$model = new $modelClass();
|
||||
$model->setAttribute('name', $normalized);
|
||||
$model->save();
|
||||
return $model->getKey();
|
||||
})
|
||||
->filter()
|
||||
->values()
|
||||
->all();
|
||||
|
||||
// Sync relation if method exists
|
||||
if (method_exists($movie, $relation)) {
|
||||
$movie->{$relation}()->sync($ids);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a person/genre/studio/etc name for de-duplication.
|
||||
*/
|
||||
protected function normalizeName(string $name): string
|
||||
{
|
||||
// Trim, collapse internal whitespace to single spaces
|
||||
$name = trim(preg_replace('/\s+/u', ' ', $name) ?? '');
|
||||
// Leave original casing (provider’s) but comparisons will be lowercase.
|
||||
return $name;
|
||||
}
|
||||
}
|
||||
26
app/Modules/Movies/routes/admin.php
Normal file
26
app/Modules/Movies/routes/admin.php
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Modules\Movies\Http\Controllers\Admin\SearchMoviesController;
|
||||
use App\Modules\Movies\Http\Controllers\Admin\AcceptMovieController;
|
||||
use App\Modules\Movies\Http\Controllers\Admin\MoviesPageController;
|
||||
use App\Modules\Movies\Http\Controllers\Admin\MoviesListPageController;
|
||||
use App\Modules\Movies\Http\Controllers\Admin\ExistsMovieController;
|
||||
use App\Modules\Movies\Http\Controllers\Admin\EditMoviePageController;
|
||||
use App\Modules\Movies\Http\Controllers\Admin\UpdateMovieController;
|
||||
use App\Modules\Movies\Http\Controllers\Admin\DeleteMovieController;
|
||||
|
||||
// Admin Movies list page (shows local DB movies)
|
||||
Route::get('/', [MoviesListPageController::class, '__invoke'])->name('index');
|
||||
|
||||
// Admin Add Movie page (OMDb search + accept flow)
|
||||
Route::get('add', [MoviesPageController::class, '__invoke'])->name('add');
|
||||
|
||||
Route::get('search', [SearchMoviesController::class, '__invoke'])->name('search');
|
||||
Route::post('accept', [AcceptMovieController::class, 'store'])->name('accept');
|
||||
Route::get('exists', [ExistsMovieController::class, '__invoke'])->name('exists');
|
||||
|
||||
// Admin edit/update/delete
|
||||
Route::get('{movie}/edit', [EditMoviePageController::class, '__invoke'])->name('edit');
|
||||
Route::patch('{movie}', [UpdateMovieController::class, 'update'])->name('update');
|
||||
Route::delete('{movie}', [DeleteMovieController::class, 'destroy'])->name('destroy');
|
||||
9
app/Modules/Movies/routes/api.php
Normal file
9
app/Modules/Movies/routes/api.php
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Modules\Movies\Http\Controllers\Browse\MoviesIndexController;
|
||||
use App\Modules\Movies\Http\Controllers\Browse\MovieShowController;
|
||||
|
||||
// Public JSON API for movies
|
||||
Route::get('/', [MoviesIndexController::class, '__invoke'])->name('index');
|
||||
Route::get('{movie}', [MovieShowController::class, '__invoke'])->name('show');
|
||||
6
app/Modules/Movies/routes/api_actors.php
Normal file
6
app/Modules/Movies/routes/api_actors.php
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Modules\Movies\Http\Controllers\Browse\Entities\ActorShowController;
|
||||
|
||||
Route::get('{actor}', [ActorShowController::class, '__invoke'])->name('show');
|
||||
6
app/Modules/Movies/routes/api_directors.php
Normal file
6
app/Modules/Movies/routes/api_directors.php
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Modules\Movies\Http\Controllers\Browse\Entities\DirectorShowController;
|
||||
|
||||
Route::get('{director}', [DirectorShowController::class, '__invoke'])->name('show');
|
||||
6
app/Modules/Movies/routes/api_genres.php
Normal file
6
app/Modules/Movies/routes/api_genres.php
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Modules\Movies\Http\Controllers\Browse\Entities\GenreShowController;
|
||||
|
||||
Route::get('{genre}', [GenreShowController::class, '__invoke'])->name('show');
|
||||
6
app/Modules/Movies/routes/api_studios.php
Normal file
6
app/Modules/Movies/routes/api_studios.php
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Modules\Movies\Http\Controllers\Browse\Entities\StudioShowController;
|
||||
|
||||
Route::get('{studio}', [StudioShowController::class, '__invoke'])->name('show');
|
||||
9
app/Modules/Movies/routes/pages.php
Normal file
9
app/Modules/Movies/routes/pages.php
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Modules\Movies\Http\Controllers\Pages\PublicMoviesPageController;
|
||||
use App\Modules\Movies\Http\Controllers\Pages\PublicMovieShowPageController;
|
||||
|
||||
// Public Inertia pages
|
||||
Route::get('/', [PublicMoviesPageController::class, '__invoke'])->name('index');
|
||||
Route::get('{movie}', [PublicMovieShowPageController::class, '__invoke'])->name('show');
|
||||
6
app/Modules/Movies/routes/pages_actors.php
Normal file
6
app/Modules/Movies/routes/pages_actors.php
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Modules\Movies\Http\Controllers\Pages\ActorsShowPageController;
|
||||
|
||||
Route::get('{actor}', [ActorsShowPageController::class, '__invoke'])->name('show');
|
||||
6
app/Modules/Movies/routes/pages_directors.php
Normal file
6
app/Modules/Movies/routes/pages_directors.php
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Modules\Movies\Http\Controllers\Pages\DirectorsShowPageController;
|
||||
|
||||
Route::get('{director}', [DirectorsShowPageController::class, '__invoke'])->name('show');
|
||||
6
app/Modules/Movies/routes/pages_genres.php
Normal file
6
app/Modules/Movies/routes/pages_genres.php
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Modules\Movies\Http\Controllers\Pages\GenresShowPageController;
|
||||
|
||||
Route::get('{genre}', [GenresShowPageController::class, '__invoke'])->name('show');
|
||||
6
app/Modules/Movies/routes/pages_studios.php
Normal file
6
app/Modules/Movies/routes/pages_studios.php
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Modules\Movies\Http\Controllers\Pages\StudiosShowPageController;
|
||||
|
||||
Route::get('{studio}', [StudiosShowPageController::class, '__invoke'])->name('show');
|
||||
8
app/Modules/Movies/routes/public.php
Normal file
8
app/Modules/Movies/routes/public.php
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Modules\Movies\Http\Controllers\Browse\MoviesIndexController;
|
||||
use App\Modules\Movies\Http\Controllers\Browse\MovieShowController;
|
||||
|
||||
Route::get('/', [MoviesIndexController::class, '__invoke'])->name('index');
|
||||
Route::get('{movie}', [MovieShowController::class, '__invoke'])->name('show');
|
||||
24
app/Providers/AppServiceProvider.php
Normal file
24
app/Providers/AppServiceProvider.php
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
91
app/Providers/FortifyServiceProvider.php
Normal file
91
app/Providers/FortifyServiceProvider.php
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Actions\Fortify\CreateNewUser;
|
||||
use App\Actions\Fortify\ResetUserPassword;
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Str;
|
||||
use Inertia\Inertia;
|
||||
use Laravel\Fortify\Features;
|
||||
use Laravel\Fortify\Fortify;
|
||||
|
||||
class FortifyServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
$this->configureActions();
|
||||
$this->configureViews();
|
||||
$this->configureRateLimiting();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure Fortify actions.
|
||||
*/
|
||||
private function configureActions(): void
|
||||
{
|
||||
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
|
||||
Fortify::createUsersUsing(CreateNewUser::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure Fortify views.
|
||||
*/
|
||||
private function configureViews(): void
|
||||
{
|
||||
Fortify::loginView(fn (Request $request) => Inertia::render('auth/Login', [
|
||||
'canResetPassword' => Features::enabled(Features::resetPasswords()),
|
||||
'canRegister' => Features::enabled(Features::registration()),
|
||||
'status' => $request->session()->get('status'),
|
||||
]));
|
||||
|
||||
Fortify::resetPasswordView(fn (Request $request) => Inertia::render('auth/ResetPassword', [
|
||||
'email' => $request->email,
|
||||
'token' => $request->route('token'),
|
||||
]));
|
||||
|
||||
Fortify::requestPasswordResetLinkView(fn (Request $request) => Inertia::render('auth/ForgotPassword', [
|
||||
'status' => $request->session()->get('status'),
|
||||
]));
|
||||
|
||||
Fortify::verifyEmailView(fn (Request $request) => Inertia::render('auth/VerifyEmail', [
|
||||
'status' => $request->session()->get('status'),
|
||||
]));
|
||||
|
||||
Fortify::registerView(fn () => Inertia::render('auth/Register'));
|
||||
|
||||
Fortify::twoFactorChallengeView(fn () => Inertia::render('auth/TwoFactorChallenge'));
|
||||
|
||||
Fortify::confirmPasswordView(fn () => Inertia::render('auth/ConfirmPassword'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure rate limiting.
|
||||
*/
|
||||
private function configureRateLimiting(): void
|
||||
{
|
||||
RateLimiter::for('two-factor', function (Request $request) {
|
||||
return Limit::perMinute(5)->by($request->session()->get('login.id'));
|
||||
});
|
||||
|
||||
RateLimiter::for('login', function (Request $request) {
|
||||
$throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())).'|'.$request->ip());
|
||||
|
||||
return Limit::perMinute(5)->by($throttleKey);
|
||||
});
|
||||
}
|
||||
}
|
||||
18
artisan
Executable file
18
artisan
Executable file
|
|
@ -0,0 +1,18 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
// Register the Composer autoloader...
|
||||
require __DIR__.'/vendor/autoload.php';
|
||||
|
||||
// Bootstrap Laravel and handle the command...
|
||||
/** @var Application $app */
|
||||
$app = require_once __DIR__.'/bootstrap/app.php';
|
||||
|
||||
$status = $app->handleCommand(new ArgvInput);
|
||||
|
||||
exit($status);
|
||||
30
bootstrap/app.php
Normal file
30
bootstrap/app.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
use App\Http\Middleware\HandleAppearance;
|
||||
use App\Http\Middleware\HandleInertiaRequests;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
use Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets;
|
||||
|
||||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
web: __DIR__.'/../routes/web.php',
|
||||
commands: __DIR__.'/../routes/console.php',
|
||||
health: '/up',
|
||||
)
|
||||
->withProviders([
|
||||
App\Modules\Movies\Providers\MoviesServiceProvider::class,
|
||||
])
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
$middleware->encryptCookies(except: ['appearance', 'sidebar_state']);
|
||||
|
||||
$middleware->web(append: [
|
||||
HandleAppearance::class,
|
||||
HandleInertiaRequests::class,
|
||||
AddLinkHeadersForPreloadedAssets::class,
|
||||
]);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
//
|
||||
})->create();
|
||||
2
bootstrap/cache/.gitignore
vendored
Normal file
2
bootstrap/cache/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
||||
6
bootstrap/providers.php
Normal file
6
bootstrap/providers.php
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
App\Providers\FortifyServiceProvider::class,
|
||||
];
|
||||
19
components.json
Normal file
19
components.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"$schema": "https://shadcn-vue.com/schema.json",
|
||||
"style": "default",
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "resources/css/app.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"composables": "@/composables",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
100
composer.json
Normal file
100
composer.json
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
{
|
||||
"$schema": "https://getcomposer.org/schema.json",
|
||||
"name": "laravel/vue-starter-kit",
|
||||
"type": "project",
|
||||
"description": "The skeleton application for the Laravel framework.",
|
||||
"keywords": [
|
||||
"laravel",
|
||||
"framework"
|
||||
],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"inertiajs/inertia-laravel": "^2.0",
|
||||
"laravel/fortify": "^1.30",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/tinker": "^2.10.1",
|
||||
"laravel/wayfinder": "^0.1.9"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.23",
|
||||
"laravel/boost": "^1.8",
|
||||
"laravel/pail": "^1.2.2",
|
||||
"laravel/pint": "^1.24",
|
||||
"laravel/sail": "^1.41",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.6",
|
||||
"pestphp/pest": "^4.1",
|
||||
"pestphp/pest-plugin-laravel": "^4.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"Database\\Factories\\": "database/factories/",
|
||||
"Database\\Seeders\\": "database/seeders/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"setup": [
|
||||
"composer install",
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
|
||||
"@php artisan key:generate",
|
||||
"@php artisan migrate --force",
|
||||
"npm install",
|
||||
"npm run build"
|
||||
],
|
||||
"dev": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
|
||||
],
|
||||
"dev:ssr": [
|
||||
"npm run build:ssr",
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"php artisan inertia:start-ssr\" --names=server,queue,logs,ssr --kill-others"
|
||||
],
|
||||
"test": [
|
||||
"@php artisan config:clear --ansi",
|
||||
"@php artisan test"
|
||||
],
|
||||
"post-autoload-dump": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||
"@php artisan package:discover --ansi"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@php artisan vendor:publish --tag=laravel-assets --ansi --force",
|
||||
"@php artisan boost:update --ansi"
|
||||
],
|
||||
"post-root-package-install": [
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||
],
|
||||
"post-create-project-cmd": [
|
||||
"@php artisan key:generate --ansi",
|
||||
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
|
||||
"@php artisan migrate --graceful --ansi"
|
||||
],
|
||||
"pre-package-uninstall": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"dont-discover": []
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true,
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
||||
9961
composer.lock
generated
Normal file
9961
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
126
config/app.php
Normal file
126
config/app.php
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value is the name of your application, which will be used when the
|
||||
| framework needs to place the application's name in a notification or
|
||||
| other UI elements where an application name needs to be displayed.
|
||||
|
|
||||
*/
|
||||
|
||||
'name' => env('APP_NAME', 'Laravel'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Environment
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the "environment" your application is currently
|
||||
| running in. This may determine how you prefer to configure various
|
||||
| services the application utilizes. Set this in your ".env" file.
|
||||
|
|
||||
*/
|
||||
|
||||
'env' => env('APP_ENV', 'production'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Debug Mode
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When your application is in debug mode, detailed error messages with
|
||||
| stack traces will be shown on every error that occurs within your
|
||||
| application. If disabled, a simple generic error page is shown.
|
||||
|
|
||||
*/
|
||||
|
||||
'debug' => (bool) env('APP_DEBUG', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application URL
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This URL is used by the console to properly generate URLs when using
|
||||
| the Artisan command line tool. You should set this to the root of
|
||||
| the application so that it's available within Artisan commands.
|
||||
|
|
||||
*/
|
||||
|
||||
'url' => env('APP_URL', 'http://localhost'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Timezone
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the default timezone for your application, which
|
||||
| will be used by the PHP date and date-time functions. The timezone
|
||||
| is set to "UTC" by default as it is suitable for most use cases.
|
||||
|
|
||||
*/
|
||||
|
||||
'timezone' => 'America/Chicago',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Locale Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The application locale determines the default locale that will be used
|
||||
| by Laravel's translation / localization methods. This option can be
|
||||
| set to any locale for which you plan to have translation strings.
|
||||
|
|
||||
*/
|
||||
|
||||
'locale' => env('APP_LOCALE', 'en'),
|
||||
|
||||
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
|
||||
|
||||
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Encryption Key
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This key is utilized by Laravel's encryption services and should be set
|
||||
| to a random, 32 character string to ensure that all encrypted values
|
||||
| are secure. You should do this prior to deploying the application.
|
||||
|
|
||||
*/
|
||||
|
||||
'cipher' => 'AES-256-CBC',
|
||||
|
||||
'key' => env('APP_KEY'),
|
||||
|
||||
'previous_keys' => [
|
||||
...array_filter(
|
||||
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
|
||||
),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Maintenance Mode Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These configuration options determine the driver used to determine and
|
||||
| manage Laravel's "maintenance mode" status. The "cache" driver will
|
||||
| allow maintenance mode to be controlled across multiple machines.
|
||||
|
|
||||
| Supported drivers: "file", "cache"
|
||||
|
|
||||
*/
|
||||
|
||||
'maintenance' => [
|
||||
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
|
||||
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||
],
|
||||
|
||||
];
|
||||
115
config/auth.php
Normal file
115
config/auth.php
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Defaults
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option defines the default authentication "guard" and password
|
||||
| reset "broker" for your application. You may change these values
|
||||
| as required, but they're a perfect start for most applications.
|
||||
|
|
||||
*/
|
||||
|
||||
'defaults' => [
|
||||
'guard' => env('AUTH_GUARD', 'web'),
|
||||
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Guards
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Next, you may define every authentication guard for your application.
|
||||
| Of course, a great default configuration has been defined for you
|
||||
| which utilizes session storage plus the Eloquent user provider.
|
||||
|
|
||||
| All authentication guards have a user provider, which defines how the
|
||||
| users are actually retrieved out of your database or other storage
|
||||
| system used by the application. Typically, Eloquent is utilized.
|
||||
|
|
||||
| Supported: "session"
|
||||
|
|
||||
*/
|
||||
|
||||
'guards' => [
|
||||
'web' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'users',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| User Providers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| All authentication guards have a user provider, which defines how the
|
||||
| users are actually retrieved out of your database or other storage
|
||||
| system used by the application. Typically, Eloquent is utilized.
|
||||
|
|
||||
| If you have multiple user tables or models you may configure multiple
|
||||
| providers to represent the model / table. These providers may then
|
||||
| be assigned to any extra authentication guards you have defined.
|
||||
|
|
||||
| Supported: "database", "eloquent"
|
||||
|
|
||||
*/
|
||||
|
||||
'providers' => [
|
||||
'users' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => env('AUTH_MODEL', App\Models\User::class),
|
||||
],
|
||||
|
||||
// 'users' => [
|
||||
// 'driver' => 'database',
|
||||
// 'table' => 'users',
|
||||
// ],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Resetting Passwords
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These configuration options specify the behavior of Laravel's password
|
||||
| reset functionality, including the table utilized for token storage
|
||||
| and the user provider that is invoked to actually retrieve users.
|
||||
|
|
||||
| The expiry time is the number of minutes that each reset token will be
|
||||
| considered valid. This security feature keeps tokens short-lived so
|
||||
| they have less time to be guessed. You may change this as needed.
|
||||
|
|
||||
| The throttle setting is the number of seconds a user must wait before
|
||||
| generating more password reset tokens. This prevents the user from
|
||||
| quickly generating a very large amount of password reset tokens.
|
||||
|
|
||||
*/
|
||||
|
||||
'passwords' => [
|
||||
'users' => [
|
||||
'provider' => 'users',
|
||||
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
|
||||
'expire' => 60,
|
||||
'throttle' => 60,
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Password Confirmation Timeout
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define the number of seconds before a password confirmation
|
||||
| window expires and users are asked to re-enter their password via the
|
||||
| confirmation screen. By default, the timeout lasts for three hours.
|
||||
|
|
||||
*/
|
||||
|
||||
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
|
||||
|
||||
];
|
||||
117
config/cache.php
Normal file
117
config/cache.php
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Cache Store
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default cache store that will be used by the
|
||||
| framework. This connection is utilized if another isn't explicitly
|
||||
| specified when running a cache operation inside the application.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('CACHE_STORE', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Stores
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define all of the cache "stores" for your application as
|
||||
| well as their drivers. You may even define multiple stores for the
|
||||
| same cache driver to group types of items stored in your caches.
|
||||
|
|
||||
| Supported drivers: "array", "database", "file", "memcached",
|
||||
| "redis", "dynamodb", "octane",
|
||||
| "failover", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'stores' => [
|
||||
|
||||
'array' => [
|
||||
'driver' => 'array',
|
||||
'serialize' => false,
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'connection' => env('DB_CACHE_CONNECTION'),
|
||||
'table' => env('DB_CACHE_TABLE', 'cache'),
|
||||
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
|
||||
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
|
||||
],
|
||||
|
||||
'file' => [
|
||||
'driver' => 'file',
|
||||
'path' => storage_path('framework/cache/data'),
|
||||
'lock_path' => storage_path('framework/cache/data'),
|
||||
],
|
||||
|
||||
'memcached' => [
|
||||
'driver' => 'memcached',
|
||||
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
|
||||
'sasl' => [
|
||||
env('MEMCACHED_USERNAME'),
|
||||
env('MEMCACHED_PASSWORD'),
|
||||
],
|
||||
'options' => [
|
||||
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
|
||||
],
|
||||
'servers' => [
|
||||
[
|
||||
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
|
||||
'port' => env('MEMCACHED_PORT', 11211),
|
||||
'weight' => 100,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
|
||||
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
|
||||
],
|
||||
|
||||
'dynamodb' => [
|
||||
'driver' => 'dynamodb',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
|
||||
'endpoint' => env('DYNAMODB_ENDPOINT'),
|
||||
],
|
||||
|
||||
'octane' => [
|
||||
'driver' => 'octane',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'driver' => 'failover',
|
||||
'stores' => [
|
||||
'database',
|
||||
'array',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Key Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
|
||||
| stores, there might be other applications using the same cache. For
|
||||
| that reason, you may prefix every cache key to avoid collisions.
|
||||
|
|
||||
*/
|
||||
|
||||
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
|
||||
|
||||
];
|
||||
183
config/database.php
Normal file
183
config/database.php
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Database Connection Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify which of the database connections below you wish
|
||||
| to use as your default connection for database operations. This is
|
||||
| the connection which will be utilized unless another connection
|
||||
| is explicitly specified when you execute a query / statement.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('DB_CONNECTION', 'sqlite'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Database Connections
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Below are all of the database connections defined for your application.
|
||||
| An example configuration is provided for each database system which
|
||||
| is supported by Laravel. You're free to add / remove connections.
|
||||
|
|
||||
*/
|
||||
|
||||
'connections' => [
|
||||
|
||||
'sqlite' => [
|
||||
'driver' => 'sqlite',
|
||||
'url' => env('DB_URL'),
|
||||
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
||||
'prefix' => '',
|
||||
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||
'busy_timeout' => null,
|
||||
'journal_mode' => null,
|
||||
'synchronous' => null,
|
||||
'transaction_mode' => 'DEFERRED',
|
||||
],
|
||||
|
||||
'mysql' => [
|
||||
'driver' => 'mysql',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '3306'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'mariadb' => [
|
||||
'driver' => 'mariadb',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '3306'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'pgsql' => [
|
||||
'driver' => 'pgsql',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '5432'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'search_path' => 'public',
|
||||
'sslmode' => 'prefer',
|
||||
],
|
||||
|
||||
'sqlsrv' => [
|
||||
'driver' => 'sqlsrv',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', 'localhost'),
|
||||
'port' => env('DB_PORT', '1433'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
|
||||
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Migration Repository Table
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This table keeps track of all the migrations that have already run for
|
||||
| your application. Using this information, we can determine which of
|
||||
| the migrations on disk haven't actually been run on the database.
|
||||
|
|
||||
*/
|
||||
|
||||
'migrations' => [
|
||||
'table' => 'migrations',
|
||||
'update_date_on_publish' => true,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Redis Databases
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Redis is an open source, fast, and advanced key-value store that also
|
||||
| provides a richer body of commands than a typical key-value system
|
||||
| such as Memcached. You may define your connection settings here.
|
||||
|
|
||||
*/
|
||||
|
||||
'redis' => [
|
||||
|
||||
'client' => env('REDIS_CLIENT', 'phpredis'),
|
||||
|
||||
'options' => [
|
||||
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
||||
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
|
||||
'persistent' => env('REDIS_PERSISTENT', false),
|
||||
],
|
||||
|
||||
'default' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'username' => env('REDIS_USERNAME'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_DB', '0'),
|
||||
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||
],
|
||||
|
||||
'cache' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'username' => env('REDIS_USERNAME'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_CACHE_DB', '1'),
|
||||
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue