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?
|
# TODO: where does this rule come from?
|
||||||
test/
|
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