update project
This commit is contained in:
+100
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"$schema": "https://getcomposer.org/schema.json",
|
||||
"name": "orchestra/workbench",
|
||||
"description": "Workbench Companion for Laravel Packages Development",
|
||||
"type": "library",
|
||||
"keywords": ["laravel", "laravel-packages", "testing", "dev"],
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Mior Muhammad Zaki",
|
||||
"email": "crynobone@gmail.com"
|
||||
}
|
||||
],
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Orchestra\\Workbench\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Orchestra\\Workbench\\Tests\\": "tests/",
|
||||
"Workbench\\App\\": "workbench/app/",
|
||||
"Workbench\\Database\\Factories\\": "workbench/database/factories/",
|
||||
"Workbench\\Database\\Seeders\\": "workbench/database/seeders/"
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.3",
|
||||
"composer-runtime-api": "^2.2",
|
||||
"fakerphp/faker": "^1.23",
|
||||
"laravel/framework": "^13.0.0",
|
||||
"laravel/pail": "^1.2.5",
|
||||
"laravel/tinker": "^3.0.0",
|
||||
"nunomaduro/collision": "^8.9",
|
||||
"orchestra/canvas": "^11.0.1",
|
||||
"orchestra/sidekick": "~1.1.23|~1.2.20",
|
||||
"orchestra/testbench-core": "^11.1.0",
|
||||
"symfony/process": "^7.4|^8.0",
|
||||
"symfony/yaml": "^7.4|^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "^1.22.0",
|
||||
"mockery/mockery": "^1.6.12",
|
||||
"phpstan/phpstan": "^2.1.33",
|
||||
"phpunit/phpunit": "^11.5.50|^12.5.8|^13.0.0",
|
||||
"spatie/laravel-ray": "^1.43.6"
|
||||
},
|
||||
"config": {
|
||||
"audit": {
|
||||
"ignore": [
|
||||
"CVE-2025-64500",
|
||||
"GHSA-vvj3-c3rp-c85p"
|
||||
]
|
||||
},
|
||||
"sort-packages": true
|
||||
},
|
||||
"suggest": {
|
||||
"ext-pcntl": "Required to use all features of the console signal trapping."
|
||||
},
|
||||
"scripts": {
|
||||
"post-autoload-dump": [
|
||||
"@clear",
|
||||
"@prepare"
|
||||
],
|
||||
"clear": "@php vendor/bin/testbench package:purge-skeleton --ansi",
|
||||
"prepare": "@php vendor/bin/testbench package:discover --ansi",
|
||||
"build": "@php vendor/bin/testbench workbench:build --ansi",
|
||||
"build:assets": [
|
||||
"npm ci",
|
||||
"npm run build"
|
||||
],
|
||||
"build:dev-assets": [
|
||||
"npm ci",
|
||||
"npm run dev"
|
||||
],
|
||||
"serve": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"@build",
|
||||
"@php vendor/bin/testbench serve --ansi"
|
||||
],
|
||||
"lint": [
|
||||
"@php vendor/bin/pint --ansi",
|
||||
"@php vendor/bin/phpstan analyse --verbose"
|
||||
],
|
||||
"test": "@php vendor/bin/phpunit --no-coverage --no-configuration --bootstrap vendor/autoload.php --color tests",
|
||||
"sync": [
|
||||
"@php bin/sync",
|
||||
"@php vendor/bin/pint --ansi",
|
||||
"npm ci",
|
||||
"npm run build"
|
||||
],
|
||||
"ci": [
|
||||
"@prepare",
|
||||
"@lint",
|
||||
"@test"
|
||||
]
|
||||
},
|
||||
"prefer-stable": true,
|
||||
"minimum-stability": "dev"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"resources/css/app.css": {
|
||||
"file": "assets/app-BeZs0uRb.css",
|
||||
"src": "resources/css/app.css",
|
||||
"isEntry": true
|
||||
},
|
||||
"resources/js/app.js": {
|
||||
"file": "assets/app-CraKG6rH.js",
|
||||
"name": "app",
|
||||
"src": "resources/js/app.js",
|
||||
"isEntry": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -0,0 +1,7 @@
|
||||
import './bootstrap';
|
||||
|
||||
import Alpine from 'alpinejs';
|
||||
|
||||
window.Alpine = Alpine;
|
||||
|
||||
Alpine.start();
|
||||
@@ -0,0 +1 @@
|
||||
// Bootstrap.js
|
||||
@@ -0,0 +1,27 @@
|
||||
<x-guest-layout>
|
||||
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ __('This is a secure area of the application. Please confirm your password before continuing.') }}
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('password.confirm') }}">
|
||||
@csrf
|
||||
|
||||
<!-- Password -->
|
||||
<div>
|
||||
<x-input-label for="password" :value="__('Password')" />
|
||||
|
||||
<x-text-input id="password" class="block mt-1 w-full"
|
||||
type="password"
|
||||
name="password"
|
||||
required autocomplete="current-password" />
|
||||
|
||||
<x-input-error :messages="$errors->get('password')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end mt-4">
|
||||
<x-primary-button>
|
||||
{{ __('Confirm') }}
|
||||
</x-primary-button>
|
||||
</div>
|
||||
</form>
|
||||
</x-guest-layout>
|
||||
@@ -0,0 +1,25 @@
|
||||
<x-guest-layout>
|
||||
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ __('Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.') }}
|
||||
</div>
|
||||
|
||||
<!-- Session Status -->
|
||||
<x-auth-session-status class="mb-4" :status="session('status')" />
|
||||
|
||||
<form method="POST" action="{{ route('password.email') }}">
|
||||
@csrf
|
||||
|
||||
<!-- Email Address -->
|
||||
<div>
|
||||
<x-input-label for="email" :value="__('Email')" />
|
||||
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus />
|
||||
<x-input-error :messages="$errors->get('email')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<x-primary-button>
|
||||
{{ __('Email Password Reset Link') }}
|
||||
</x-primary-button>
|
||||
</div>
|
||||
</form>
|
||||
</x-guest-layout>
|
||||
@@ -0,0 +1,47 @@
|
||||
<x-guest-layout>
|
||||
<!-- Session Status -->
|
||||
<x-auth-session-status class="mb-4" :status="session('status')" />
|
||||
|
||||
<form method="POST" action="{{ route('login') }}">
|
||||
@csrf
|
||||
|
||||
<!-- Email Address -->
|
||||
<div>
|
||||
<x-input-label for="email" :value="__('Email')" />
|
||||
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus autocomplete="username" />
|
||||
<x-input-error :messages="$errors->get('email')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="mt-4">
|
||||
<x-input-label for="password" :value="__('Password')" />
|
||||
|
||||
<x-text-input id="password" class="block mt-1 w-full"
|
||||
type="password"
|
||||
name="password"
|
||||
required autocomplete="current-password" />
|
||||
|
||||
<x-input-error :messages="$errors->get('password')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Remember Me -->
|
||||
<div class="block mt-4">
|
||||
<label for="remember_me" class="inline-flex items-center">
|
||||
<input id="remember_me" type="checkbox" class="rounded dark:bg-gray-900 border-gray-300 dark:border-gray-700 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:focus:ring-offset-gray-800" name="remember">
|
||||
<span class="ms-2 text-sm text-gray-600 dark:text-gray-400">{{ __('Remember me') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
@if (Route::has('password.request'))
|
||||
<a class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800" href="{{ route('password.request') }}">
|
||||
{{ __('Forgot your password?') }}
|
||||
</a>
|
||||
@endif
|
||||
|
||||
<x-primary-button class="ms-3">
|
||||
{{ __('Log in') }}
|
||||
</x-primary-button>
|
||||
</div>
|
||||
</form>
|
||||
</x-guest-layout>
|
||||
@@ -0,0 +1,52 @@
|
||||
<x-guest-layout>
|
||||
<form method="POST" action="{{ route('register') }}">
|
||||
@csrf
|
||||
|
||||
<!-- Name -->
|
||||
<div>
|
||||
<x-input-label for="name" :value="__('Name')" />
|
||||
<x-text-input id="name" class="block mt-1 w-full" type="text" name="name" :value="old('name')" required autofocus autocomplete="name" />
|
||||
<x-input-error :messages="$errors->get('name')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Email Address -->
|
||||
<div class="mt-4">
|
||||
<x-input-label for="email" :value="__('Email')" />
|
||||
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autocomplete="username" />
|
||||
<x-input-error :messages="$errors->get('email')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="mt-4">
|
||||
<x-input-label for="password" :value="__('Password')" />
|
||||
|
||||
<x-text-input id="password" class="block mt-1 w-full"
|
||||
type="password"
|
||||
name="password"
|
||||
required autocomplete="new-password" />
|
||||
|
||||
<x-input-error :messages="$errors->get('password')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Confirm Password -->
|
||||
<div class="mt-4">
|
||||
<x-input-label for="password_confirmation" :value="__('Confirm Password')" />
|
||||
|
||||
<x-text-input id="password_confirmation" class="block mt-1 w-full"
|
||||
type="password"
|
||||
name="password_confirmation" required autocomplete="new-password" />
|
||||
|
||||
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<a class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800" href="{{ route('login') }}">
|
||||
{{ __('Already registered?') }}
|
||||
</a>
|
||||
|
||||
<x-primary-button class="ms-4">
|
||||
{{ __('Register') }}
|
||||
</x-primary-button>
|
||||
</div>
|
||||
</form>
|
||||
</x-guest-layout>
|
||||
@@ -0,0 +1,39 @@
|
||||
<x-guest-layout>
|
||||
<form method="POST" action="{{ route('password.store') }}">
|
||||
@csrf
|
||||
|
||||
<!-- Password Reset Token -->
|
||||
<input type="hidden" name="token" value="{{ $request->route('token') }}">
|
||||
|
||||
<!-- Email Address -->
|
||||
<div>
|
||||
<x-input-label for="email" :value="__('Email')" />
|
||||
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email', $request->email)" required autofocus autocomplete="username" />
|
||||
<x-input-error :messages="$errors->get('email')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="mt-4">
|
||||
<x-input-label for="password" :value="__('Password')" />
|
||||
<x-text-input id="password" class="block mt-1 w-full" type="password" name="password" required autocomplete="new-password" />
|
||||
<x-input-error :messages="$errors->get('password')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Confirm Password -->
|
||||
<div class="mt-4">
|
||||
<x-input-label for="password_confirmation" :value="__('Confirm Password')" />
|
||||
|
||||
<x-text-input id="password_confirmation" class="block mt-1 w-full"
|
||||
type="password"
|
||||
name="password_confirmation" required autocomplete="new-password" />
|
||||
|
||||
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<x-primary-button>
|
||||
{{ __('Reset Password') }}
|
||||
</x-primary-button>
|
||||
</div>
|
||||
</form>
|
||||
</x-guest-layout>
|
||||
@@ -0,0 +1,31 @@
|
||||
<x-guest-layout>
|
||||
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ __('Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn\'t receive the email, we will gladly send you another.') }}
|
||||
</div>
|
||||
|
||||
@if (session('status') == 'verification-link-sent')
|
||||
<div class="mb-4 font-medium text-sm text-green-600 dark:text-green-400">
|
||||
{{ __('A new verification link has been sent to the email address you provided during registration.') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<form method="POST" action="{{ route('verification.send') }}">
|
||||
@csrf
|
||||
|
||||
<div>
|
||||
<x-primary-button>
|
||||
{{ __('Resend Verification Email') }}
|
||||
</x-primary-button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
|
||||
<button type="submit" class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800">
|
||||
{{ __('Log Out') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</x-guest-layout>
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 316 316" xmlns="http://www.w3.org/2000/svg" {{ $attributes }}>
|
||||
<path d="M305.8 81.125C305.77 80.995 305.69 80.885 305.65 80.755C305.56 80.525 305.49 80.285 305.37 80.075C305.29 79.935 305.17 79.815 305.07 79.685C304.94 79.515 304.83 79.325 304.68 79.175C304.55 79.045 304.39 78.955 304.25 78.845C304.09 78.715 303.95 78.575 303.77 78.475L251.32 48.275C249.97 47.495 248.31 47.495 246.96 48.275L194.51 78.475C194.33 78.575 194.19 78.725 194.03 78.845C193.89 78.955 193.73 79.045 193.6 79.175C193.45 79.325 193.34 79.515 193.21 79.685C193.11 79.815 192.99 79.935 192.91 80.075C192.79 80.285 192.71 80.525 192.63 80.755C192.58 80.875 192.51 80.995 192.48 81.125C192.38 81.495 192.33 81.875 192.33 82.265V139.625L148.62 164.795V52.575C148.62 52.185 148.57 51.805 148.47 51.435C148.44 51.305 148.36 51.195 148.32 51.065C148.23 50.835 148.16 50.595 148.04 50.385C147.96 50.245 147.84 50.125 147.74 49.995C147.61 49.825 147.5 49.635 147.35 49.485C147.22 49.355 147.06 49.265 146.92 49.155C146.76 49.025 146.62 48.885 146.44 48.785L93.99 18.585C92.64 17.805 90.98 17.805 89.63 18.585L37.18 48.785C37 48.885 36.86 49.035 36.7 49.155C36.56 49.265 36.4 49.355 36.27 49.485C36.12 49.635 36.01 49.825 35.88 49.995C35.78 50.125 35.66 50.245 35.58 50.385C35.46 50.595 35.38 50.835 35.3 51.065C35.25 51.185 35.18 51.305 35.15 51.435C35.05 51.805 35 52.185 35 52.575V232.235C35 233.795 35.84 235.245 37.19 236.025L142.1 296.425C142.33 296.555 142.58 296.635 142.82 296.725C142.93 296.765 143.04 296.835 143.16 296.865C143.53 296.965 143.9 297.015 144.28 297.015C144.66 297.015 145.03 296.965 145.4 296.865C145.5 296.835 145.59 296.775 145.69 296.745C145.95 296.655 146.21 296.565 146.45 296.435L251.36 236.035C252.72 235.255 253.55 233.815 253.55 232.245V174.885L303.81 145.945C305.17 145.165 306 143.725 306 142.155V82.265C305.95 81.875 305.89 81.495 305.8 81.125ZM144.2 227.205L100.57 202.515L146.39 176.135L196.66 147.195L240.33 172.335L208.29 190.625L144.2 227.205ZM244.75 114.995V164.795L226.39 154.225L201.03 139.625V89.825L219.39 100.395L244.75 114.995ZM249.12 57.105L292.81 82.265L249.12 107.425L205.43 82.265L249.12 57.105ZM114.49 184.425L96.13 194.995V85.305L121.49 70.705L139.85 60.135V169.815L114.49 184.425ZM91.76 27.425L135.45 52.585L91.76 77.745L48.07 52.585L91.76 27.425ZM43.67 60.135L62.03 70.705L87.39 85.305V202.545V202.555V202.565C87.39 202.735 87.44 202.895 87.46 203.055C87.49 203.265 87.49 203.485 87.55 203.695V203.705C87.6 203.875 87.69 204.035 87.76 204.195C87.84 204.375 87.89 204.575 87.99 204.745C87.99 204.745 87.99 204.755 88 204.755C88.09 204.905 88.22 205.035 88.33 205.175C88.45 205.335 88.55 205.495 88.69 205.635L88.7 205.645C88.82 205.765 88.98 205.855 89.12 205.965C89.28 206.085 89.42 206.225 89.59 206.325C89.6 206.325 89.6 206.325 89.61 206.335C89.62 206.335 89.62 206.345 89.63 206.345L139.87 234.775V285.065L43.67 229.705V60.135ZM244.75 229.705L148.58 285.075V234.775L219.8 194.115L244.75 179.875V229.705ZM297.2 139.625L253.49 164.795V114.995L278.85 100.395L297.21 89.825V139.625H297.2Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
+7
@@ -0,0 +1,7 @@
|
||||
@props(['status'])
|
||||
|
||||
@if ($status)
|
||||
<div {{ $attributes->merge(['class' => 'font-medium text-sm text-green-600 dark:text-green-400']) }}>
|
||||
{{ $status }}
|
||||
</div>
|
||||
@endif
|
||||
@@ -0,0 +1,3 @@
|
||||
<button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-red-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-500 active:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150']) }}>
|
||||
{{ $slot }}
|
||||
</button>
|
||||
@@ -0,0 +1 @@
|
||||
<a {{ $attributes->merge(['class' => 'block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-800 transition duration-150 ease-in-out']) }}>{{ $slot }}</a>
|
||||
@@ -0,0 +1,35 @@
|
||||
@props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white dark:bg-gray-700'])
|
||||
|
||||
@php
|
||||
$alignmentClasses = match ($align) {
|
||||
'left' => 'ltr:origin-top-left rtl:origin-top-right start-0',
|
||||
'top' => 'origin-top',
|
||||
default => 'ltr:origin-top-right rtl:origin-top-left end-0',
|
||||
};
|
||||
|
||||
$width = match ($width) {
|
||||
'48' => 'w-48',
|
||||
default => $width,
|
||||
};
|
||||
@endphp
|
||||
|
||||
<div class="relative" x-data="{ open: false }" @click.outside="open = false" @close.stop="open = false">
|
||||
<div @click="open = ! open">
|
||||
{{ $trigger }}
|
||||
</div>
|
||||
|
||||
<div x-show="open"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 scale-95"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-95"
|
||||
class="absolute z-50 mt-2 {{ $width }} rounded-md shadow-lg {{ $alignmentClasses }}"
|
||||
style="display: none;"
|
||||
@click="open = false">
|
||||
<div class="rounded-md ring-1 ring-black ring-opacity-5 {{ $contentClasses }}">
|
||||
{{ $content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,9 @@
|
||||
@props(['messages'])
|
||||
|
||||
@if ($messages)
|
||||
<ul {{ $attributes->merge(['class' => 'text-sm text-red-600 dark:text-red-400 space-y-1']) }}>
|
||||
@foreach ((array) $messages as $message)
|
||||
<li>{{ $message }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@endif
|
||||
@@ -0,0 +1,5 @@
|
||||
@props(['value'])
|
||||
|
||||
<label {{ $attributes->merge(['class' => 'block font-medium text-sm text-gray-700 dark:text-gray-300']) }}>
|
||||
{{ $value ?? $slot }}
|
||||
</label>
|
||||
@@ -0,0 +1,78 @@
|
||||
@props([
|
||||
'name',
|
||||
'show' => false,
|
||||
'maxWidth' => '2xl'
|
||||
])
|
||||
|
||||
@php
|
||||
$maxWidth = [
|
||||
'sm' => 'sm:max-w-sm',
|
||||
'md' => 'sm:max-w-md',
|
||||
'lg' => 'sm:max-w-lg',
|
||||
'xl' => 'sm:max-w-xl',
|
||||
'2xl' => 'sm:max-w-2xl',
|
||||
][$maxWidth];
|
||||
@endphp
|
||||
|
||||
<div
|
||||
x-data="{
|
||||
show: @js($show),
|
||||
focusables() {
|
||||
// All focusable element types...
|
||||
let selector = 'a, button, input:not([type=\'hidden\']), textarea, select, details, [tabindex]:not([tabindex=\'-1\'])'
|
||||
return [...$el.querySelectorAll(selector)]
|
||||
// All non-disabled elements...
|
||||
.filter(el => ! el.hasAttribute('disabled'))
|
||||
},
|
||||
firstFocusable() { return this.focusables()[0] },
|
||||
lastFocusable() { return this.focusables().slice(-1)[0] },
|
||||
nextFocusable() { return this.focusables()[this.nextFocusableIndex()] || this.firstFocusable() },
|
||||
prevFocusable() { return this.focusables()[this.prevFocusableIndex()] || this.lastFocusable() },
|
||||
nextFocusableIndex() { return (this.focusables().indexOf(document.activeElement) + 1) % (this.focusables().length + 1) },
|
||||
prevFocusableIndex() { return Math.max(0, this.focusables().indexOf(document.activeElement)) -1 },
|
||||
}"
|
||||
x-init="$watch('show', value => {
|
||||
if (value) {
|
||||
document.body.classList.add('overflow-y-hidden');
|
||||
{{ $attributes->has('focusable') ? 'setTimeout(() => firstFocusable().focus(), 100)' : '' }}
|
||||
} else {
|
||||
document.body.classList.remove('overflow-y-hidden');
|
||||
}
|
||||
})"
|
||||
x-on:open-modal.window="$event.detail == '{{ $name }}' ? show = true : null"
|
||||
x-on:close-modal.window="$event.detail == '{{ $name }}' ? show = false : null"
|
||||
x-on:close.stop="show = false"
|
||||
x-on:keydown.escape.window="show = false"
|
||||
x-on:keydown.tab.prevent="$event.shiftKey || nextFocusable().focus()"
|
||||
x-on:keydown.shift.tab.prevent="prevFocusable().focus()"
|
||||
x-show="show"
|
||||
class="fixed inset-0 overflow-y-auto px-4 py-6 sm:px-0 z-50"
|
||||
style="display: {{ $show ? 'block' : 'none' }};"
|
||||
>
|
||||
<div
|
||||
x-show="show"
|
||||
class="fixed inset-0 transform transition-all"
|
||||
x-on:click="show = false"
|
||||
x-transition:enter="ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
>
|
||||
<div class="absolute inset-0 bg-gray-500 dark:bg-gray-900 opacity-75"></div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
x-show="show"
|
||||
class="mb-6 bg-white dark:bg-gray-800 rounded-lg overflow-hidden shadow-xl transform transition-all sm:w-full {{ $maxWidth }} sm:mx-auto"
|
||||
x-transition:enter="ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave="ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
@props(['active'])
|
||||
|
||||
@php
|
||||
$classes = ($active ?? false)
|
||||
? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 dark:border-indigo-600 text-sm font-medium leading-5 text-gray-900 dark:text-gray-100 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out'
|
||||
: 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-700 focus:outline-none focus:text-gray-700 dark:focus:text-gray-300 focus:border-gray-300 dark:focus:border-gray-700 transition duration-150 ease-in-out';
|
||||
@endphp
|
||||
|
||||
<a {{ $attributes->merge(['class' => $classes]) }}>
|
||||
{{ $slot }}
|
||||
</a>
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
<button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-gray-800 dark:bg-gray-200 border border-transparent rounded-md font-semibold text-xs text-white dark:text-gray-800 uppercase tracking-widest hover:bg-gray-700 dark:hover:bg-white focus:bg-gray-700 dark:focus:bg-white active:bg-gray-900 dark:active:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150']) }}>
|
||||
{{ $slot }}
|
||||
</button>
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
@props(['active'])
|
||||
|
||||
@php
|
||||
$classes = ($active ?? false)
|
||||
? 'block w-full ps-3 pe-4 py-2 border-l-4 border-indigo-400 dark:border-indigo-600 text-start text-base font-medium text-indigo-700 dark:text-indigo-300 bg-indigo-50 dark:bg-indigo-900/50 focus:outline-none focus:text-indigo-800 dark:focus:text-indigo-200 focus:bg-indigo-100 dark:focus:bg-indigo-900 focus:border-indigo-700 dark:focus:border-indigo-300 transition duration-150 ease-in-out'
|
||||
: 'block w-full ps-3 pe-4 py-2 border-l-4 border-transparent text-start text-base font-medium text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 hover:border-gray-300 dark:hover:border-gray-600 focus:outline-none focus:text-gray-800 dark:focus:text-gray-200 focus:bg-gray-50 dark:focus:bg-gray-700 focus:border-gray-300 dark:focus:border-gray-600 transition duration-150 ease-in-out';
|
||||
@endphp
|
||||
|
||||
<a {{ $attributes->merge(['class' => $classes]) }}>
|
||||
{{ $slot }}
|
||||
</a>
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
<button {{ $attributes->merge(['type' => 'button', 'class' => 'inline-flex items-center px-4 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-500 rounded-md font-semibold text-xs text-gray-700 dark:text-gray-300 uppercase tracking-widest shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 disabled:opacity-25 transition ease-in-out duration-150']) }}>
|
||||
{{ $slot }}
|
||||
</button>
|
||||
@@ -0,0 +1,3 @@
|
||||
@props(['disabled' => false])
|
||||
|
||||
<input @disabled($disabled) {{ $attributes->merge(['class' => 'border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm']) }}>
|
||||
@@ -0,0 +1,17 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
|
||||
{{ __('Dashboard') }}
|
||||
</h2>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6 text-gray-900 dark:text-gray-100">
|
||||
{{ __("You're logged in!") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
|
||||
<title>{{ config('app.name', 'Laravel') }}</title>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
|
||||
|
||||
<!-- Scripts -->
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
</head>
|
||||
<body class="font-sans antialiased">
|
||||
<div class="min-h-screen bg-gray-100 dark:bg-gray-900">
|
||||
@include('layouts.navigation')
|
||||
|
||||
<!-- Page Heading -->
|
||||
@isset($header)
|
||||
<header class="bg-white dark:bg-gray-800 shadow">
|
||||
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
|
||||
{{ $header }}
|
||||
</div>
|
||||
</header>
|
||||
@endisset
|
||||
|
||||
<!-- Page Content -->
|
||||
<main>
|
||||
{{ $slot }}
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
|
||||
<title>{{ config('app.name', 'Laravel') }}</title>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
|
||||
|
||||
<!-- Scripts -->
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
</head>
|
||||
<body class="font-sans text-gray-900 antialiased">
|
||||
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100 dark:bg-gray-900">
|
||||
<div>
|
||||
<a href="/">
|
||||
<x-application-logo class="w-20 h-20 fill-current text-gray-500" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white dark:bg-gray-800 shadow-md overflow-hidden sm:rounded-lg">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,100 @@
|
||||
<nav x-data="{ open: false }" class="bg-white dark:bg-gray-800 border-b border-gray-100 dark:border-gray-700">
|
||||
<!-- Primary Navigation Menu -->
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex">
|
||||
<!-- Logo -->
|
||||
<div class="shrink-0 flex items-center">
|
||||
<a href="{{ route('dashboard') }}">
|
||||
<x-application-logo class="block h-9 w-auto fill-current text-gray-800 dark:text-gray-200" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Links -->
|
||||
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
|
||||
<x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
|
||||
{{ __('Dashboard') }}
|
||||
</x-nav-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Dropdown -->
|
||||
<div class="hidden sm:flex sm:items-center sm:ms-6">
|
||||
<x-dropdown align="right" width="48">
|
||||
<x-slot name="trigger">
|
||||
<button class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-800 hover:text-gray-700 dark:hover:text-gray-300 focus:outline-none transition ease-in-out duration-150">
|
||||
<div>{{ Auth::user()->name }}</div>
|
||||
|
||||
<div class="ms-1">
|
||||
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<x-dropdown-link :href="route('profile.edit')">
|
||||
{{ __('Profile') }}
|
||||
</x-dropdown-link>
|
||||
|
||||
<!-- Authentication -->
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
|
||||
<x-dropdown-link :href="route('logout')"
|
||||
onclick="event.preventDefault();
|
||||
this.closest('form').submit();">
|
||||
{{ __('Log Out') }}
|
||||
</x-dropdown-link>
|
||||
</form>
|
||||
</x-slot>
|
||||
</x-dropdown>
|
||||
</div>
|
||||
|
||||
<!-- Hamburger -->
|
||||
<div class="-me-2 flex items-center sm:hidden">
|
||||
<button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 dark:text-gray-500 hover:text-gray-500 dark:hover:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-900 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-900 focus:text-gray-500 dark:focus:text-gray-400 transition duration-150 ease-in-out">
|
||||
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
|
||||
<path :class="{'hidden': open, 'inline-flex': ! open }" class="inline-flex" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
<path :class="{'hidden': ! open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Responsive Navigation Menu -->
|
||||
<div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
|
||||
<div class="pt-2 pb-3 space-y-1">
|
||||
<x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
|
||||
{{ __('Dashboard') }}
|
||||
</x-responsive-nav-link>
|
||||
</div>
|
||||
|
||||
<!-- Responsive Settings Options -->
|
||||
<div class="pt-4 pb-1 border-t border-gray-200 dark:border-gray-600">
|
||||
<div class="px-4">
|
||||
<div class="font-medium text-base text-gray-800 dark:text-gray-200">{{ Auth::user()->name }}</div>
|
||||
<div class="font-medium text-sm text-gray-500">{{ Auth::user()->email }}</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 space-y-1">
|
||||
<x-responsive-nav-link :href="route('profile.edit')">
|
||||
{{ __('Profile') }}
|
||||
</x-responsive-nav-link>
|
||||
|
||||
<!-- Authentication -->
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
|
||||
<x-responsive-nav-link :href="route('logout')"
|
||||
onclick="event.preventDefault();
|
||||
this.closest('form').submit();">
|
||||
{{ __('Log Out') }}
|
||||
</x-responsive-nav-link>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -0,0 +1,29 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
|
||||
{{ __('Profile') }}
|
||||
</h2>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
|
||||
<div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
|
||||
<div class="max-w-xl">
|
||||
@include('profile.partials.update-profile-information-form')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
|
||||
<div class="max-w-xl">
|
||||
@include('profile.partials.update-password-form')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
|
||||
<div class="max-w-xl">
|
||||
@include('profile.partials.delete-user-form')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
<section class="space-y-6">
|
||||
<header>
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ __('Delete Account') }}
|
||||
</h2>
|
||||
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.') }}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<x-danger-button
|
||||
x-data=""
|
||||
x-on:click.prevent="$dispatch('open-modal', 'confirm-user-deletion')"
|
||||
>{{ __('Delete Account') }}</x-danger-button>
|
||||
|
||||
<x-modal name="confirm-user-deletion" :show="$errors->userDeletion->isNotEmpty()" focusable>
|
||||
<form method="post" action="{{ route('profile.destroy') }}" class="p-6">
|
||||
@csrf
|
||||
@method('delete')
|
||||
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ __('Are you sure you want to delete your account?') }}
|
||||
</h2>
|
||||
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.') }}
|
||||
</p>
|
||||
|
||||
<div class="mt-6">
|
||||
<x-input-label for="password" value="{{ __('Password') }}" class="sr-only" />
|
||||
|
||||
<x-text-input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
class="mt-1 block w-3/4"
|
||||
placeholder="{{ __('Password') }}"
|
||||
/>
|
||||
|
||||
<x-input-error :messages="$errors->userDeletion->get('password')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end">
|
||||
<x-secondary-button x-on:click="$dispatch('close')">
|
||||
{{ __('Cancel') }}
|
||||
</x-secondary-button>
|
||||
|
||||
<x-danger-button class="ms-3">
|
||||
{{ __('Delete Account') }}
|
||||
</x-danger-button>
|
||||
</div>
|
||||
</form>
|
||||
</x-modal>
|
||||
</section>
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
<section>
|
||||
<header>
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ __('Update Password') }}
|
||||
</h2>
|
||||
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ __('Ensure your account is using a long, random password to stay secure.') }}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<form method="post" action="{{ route('password.update') }}" class="mt-6 space-y-6">
|
||||
@csrf
|
||||
@method('put')
|
||||
|
||||
<div>
|
||||
<x-input-label for="update_password_current_password" :value="__('Current Password')" />
|
||||
<x-text-input id="update_password_current_password" name="current_password" type="password" class="mt-1 block w-full" autocomplete="current-password" />
|
||||
<x-input-error :messages="$errors->updatePassword->get('current_password')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<x-input-label for="update_password_password" :value="__('New Password')" />
|
||||
<x-text-input id="update_password_password" name="password" type="password" class="mt-1 block w-full" autocomplete="new-password" />
|
||||
<x-input-error :messages="$errors->updatePassword->get('password')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<x-input-label for="update_password_password_confirmation" :value="__('Confirm Password')" />
|
||||
<x-text-input id="update_password_password_confirmation" name="password_confirmation" type="password" class="mt-1 block w-full" autocomplete="new-password" />
|
||||
<x-input-error :messages="$errors->updatePassword->get('password_confirmation')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<x-primary-button>{{ __('Save') }}</x-primary-button>
|
||||
|
||||
@if (session('status') === 'password-updated')
|
||||
<p
|
||||
x-data="{ show: true }"
|
||||
x-show="show"
|
||||
x-transition
|
||||
x-init="setTimeout(() => show = false, 2000)"
|
||||
class="text-sm text-gray-600 dark:text-gray-400"
|
||||
>{{ __('Saved.') }}</p>
|
||||
@endif
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
<section>
|
||||
<header>
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ __('Profile Information') }}
|
||||
</h2>
|
||||
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ __("Update your account's profile information and email address.") }}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<form id="send-verification" method="post" action="{{ route('verification.send') }}">
|
||||
@csrf
|
||||
</form>
|
||||
|
||||
<form method="post" action="{{ route('profile.update') }}" class="mt-6 space-y-6">
|
||||
@csrf
|
||||
@method('patch')
|
||||
|
||||
<div>
|
||||
<x-input-label for="name" :value="__('Name')" />
|
||||
<x-text-input id="name" name="name" type="text" class="mt-1 block w-full" :value="old('name', $user->name)" required autofocus autocomplete="name" />
|
||||
<x-input-error class="mt-2" :messages="$errors->get('name')" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<x-input-label for="email" :value="__('Email')" />
|
||||
<x-text-input id="email" name="email" type="email" class="mt-1 block w-full" :value="old('email', $user->email)" required autocomplete="username" />
|
||||
<x-input-error class="mt-2" :messages="$errors->get('email')" />
|
||||
|
||||
@if ($user instanceof \Illuminate\Contracts\Auth\MustVerifyEmail && ! $user->hasVerifiedEmail())
|
||||
<div>
|
||||
<p class="text-sm mt-2 text-gray-800 dark:text-gray-200">
|
||||
{{ __('Your email address is unverified.') }}
|
||||
|
||||
<button form="send-verification" class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800">
|
||||
{{ __('Click here to re-send the verification email.') }}
|
||||
</button>
|
||||
</p>
|
||||
|
||||
@if (session('status') === 'verification-link-sent')
|
||||
<p class="mt-2 font-medium text-sm text-green-600 dark:text-green-400">
|
||||
{{ __('A new verification link has been sent to your email address.') }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<x-primary-button>{{ __('Save') }}</x-primary-button>
|
||||
|
||||
@if (session('status') === 'profile-updated')
|
||||
<p
|
||||
x-data="{ show: true }"
|
||||
x-show="show"
|
||||
x-transition
|
||||
x-init="setTimeout(() => show = false, 2000)"
|
||||
class="text-sm text-gray-600 dark:text-gray-400"
|
||||
>{{ __('Saved.') }}</p>
|
||||
@endif
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Orchestra\Workbench\Http\Controllers\Auth\AuthenticatedSessionController;
|
||||
use Orchestra\Workbench\Http\Controllers\Auth\ConfirmablePasswordController;
|
||||
use Orchestra\Workbench\Http\Controllers\Auth\EmailVerificationNotificationController;
|
||||
use Orchestra\Workbench\Http\Controllers\Auth\EmailVerificationPromptController;
|
||||
use Orchestra\Workbench\Http\Controllers\Auth\NewPasswordController;
|
||||
use Orchestra\Workbench\Http\Controllers\Auth\PasswordController;
|
||||
use Orchestra\Workbench\Http\Controllers\Auth\PasswordResetLinkController;
|
||||
use Orchestra\Workbench\Http\Controllers\Auth\RegisteredUserController;
|
||||
use Orchestra\Workbench\Http\Controllers\Auth\VerifyEmailController;
|
||||
|
||||
Route::middleware('guest')->group(function () {
|
||||
Route::get('register', [RegisteredUserController::class, 'create'])
|
||||
->name('register');
|
||||
|
||||
Route::post('register', [RegisteredUserController::class, 'store']);
|
||||
|
||||
Route::get('login', [AuthenticatedSessionController::class, 'create'])
|
||||
->name('login');
|
||||
|
||||
Route::post('login', [AuthenticatedSessionController::class, 'store']);
|
||||
|
||||
Route::get('forgot-password', [PasswordResetLinkController::class, 'create'])
|
||||
->name('password.request');
|
||||
|
||||
Route::post('forgot-password', [PasswordResetLinkController::class, 'store'])
|
||||
->name('password.email');
|
||||
|
||||
Route::get('reset-password/{token}', [NewPasswordController::class, 'create'])
|
||||
->name('password.reset');
|
||||
|
||||
Route::post('reset-password', [NewPasswordController::class, 'store'])
|
||||
->name('password.store');
|
||||
});
|
||||
|
||||
Route::middleware('auth')->group(function () {
|
||||
Route::get('verify-email', EmailVerificationPromptController::class)
|
||||
->name('verification.notice');
|
||||
|
||||
Route::get('verify-email/{id}/{hash}', VerifyEmailController::class)
|
||||
->middleware(['signed', 'throttle:6,1'])
|
||||
->name('verification.verify');
|
||||
|
||||
Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store'])
|
||||
->middleware('throttle:6,1')
|
||||
->name('verification.send');
|
||||
|
||||
Route::get('confirm-password', [ConfirmablePasswordController::class, 'show'])
|
||||
->name('password.confirm');
|
||||
|
||||
Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']);
|
||||
|
||||
Route::put('password', [PasswordController::class, 'update'])->name('password.update');
|
||||
|
||||
Route::post('logout', [AuthenticatedSessionController::class, 'destroy'])
|
||||
->name('logout');
|
||||
});
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Orchestra\Workbench\Http\Controllers\ProfileController;
|
||||
|
||||
// Route::get('/', function () {
|
||||
// return view('welcome');
|
||||
// });
|
||||
|
||||
Route::get('/dashboard', function () {
|
||||
return view('dashboard');
|
||||
})->middleware(['auth', 'verified'])->name('dashboard');
|
||||
|
||||
Route::middleware('auth')->group(function () {
|
||||
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
|
||||
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
|
||||
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
|
||||
});
|
||||
|
||||
require __DIR__.'/auth.php';
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
use function Orchestra\Testbench\join_paths;
|
||||
|
||||
Route::middleware('web')
|
||||
->group(join_paths(__DIR__, 'web.php'));
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Routing\Router;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Orchestra\Workbench\Http\Controllers\WorkbenchController;
|
||||
|
||||
Route::group([
|
||||
'prefix' => '_workbench',
|
||||
'middleware' => 'web',
|
||||
], static function (Router $router) {
|
||||
$router->get(
|
||||
'/', [WorkbenchController::class, 'start']
|
||||
)->name('workbench.start');
|
||||
|
||||
$router->get(
|
||||
'/login/{userId}/{guard?}', [WorkbenchController::class, 'login']
|
||||
)->name('workbench.login');
|
||||
|
||||
$router->get(
|
||||
'/logout/{guard?}', [WorkbenchController::class, 'logout']
|
||||
)->name('workbench.logout');
|
||||
|
||||
$router->get(
|
||||
'/user/{guard?}', [WorkbenchController::class, 'user']
|
||||
)->name('workbench.user');
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench\Actions;
|
||||
|
||||
/**
|
||||
* @api
|
||||
*
|
||||
* @deprecated 7.18.0 Use `Orchestra\Canvas\Core\Actions\DumpComposerAutoloads` instead.
|
||||
*/
|
||||
class DumpComposerAutoloads extends \Orchestra\Canvas\Core\Actions\DumpComposerAutoloads
|
||||
{
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench\Actions;
|
||||
|
||||
/**
|
||||
* @api
|
||||
*/
|
||||
class ModifyComposer
|
||||
{
|
||||
/**
|
||||
* Construct a new action.
|
||||
*/
|
||||
public function __construct(
|
||||
protected string $workingPath
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Handle the action.
|
||||
*
|
||||
* @param callable(array):array $callback
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function handle(callable $callback): void
|
||||
{
|
||||
app('workbench.composer')
|
||||
->setWorkingPath($this->workingPath)
|
||||
->modify($callback);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench\Actions;
|
||||
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
use Orchestra\Workbench\Workbench;
|
||||
|
||||
/**
|
||||
* @api
|
||||
*/
|
||||
class ReplaceNamespaces
|
||||
{
|
||||
/**
|
||||
* Construct a new action.
|
||||
*/
|
||||
public function __construct(
|
||||
protected Filesystem $filesystem
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Handle the action.
|
||||
*/
|
||||
public function handle(string $filename): void
|
||||
{
|
||||
if (! $this->filesystem->isFile($filename)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$workbenchAppNamespacePrefix = rtrim(Workbench::detectNamespace('app') ?? 'Workbench\App\\', '\\');
|
||||
$workbenchFactoriesNamespacePrefix = rtrim(Workbench::detectNamespace('database/factories') ?? 'Workbench\Database\Factories\\', '\\');
|
||||
$workbenchSeederNamespacePrefix = rtrim(Workbench::detectNamespace('database/seeders') ?? 'Workbench\Database\Seeders\\', '\\');
|
||||
|
||||
$serviceProvider = \sprintf('%s\Providers\WorkbenchServiceProvider', $workbenchAppNamespacePrefix);
|
||||
$databaseSeeder = \sprintf('%s\DatabaseSeeder', $workbenchSeederNamespacePrefix);
|
||||
$userModel = \sprintf('%s\Models\User', $workbenchAppNamespacePrefix);
|
||||
$userFactory = \sprintf('%s\UserFactory', $workbenchFactoriesNamespacePrefix);
|
||||
|
||||
$keywords = [
|
||||
'Workbench\App' => $workbenchAppNamespacePrefix,
|
||||
'Workbench\Database\Factories' => $workbenchFactoriesNamespacePrefix,
|
||||
'Workbench\Database\Seeders' => $workbenchSeederNamespacePrefix,
|
||||
|
||||
'{{WorkbenchAppNamespace}}' => $workbenchAppNamespacePrefix,
|
||||
'{{ WorkbenchAppNamespace }}' => $workbenchAppNamespacePrefix,
|
||||
'{{WorkbenchFactoryNamespace}}' => $workbenchFactoriesNamespacePrefix,
|
||||
'{{ WorkbenchFactoryNamespace }}' => $workbenchFactoriesNamespacePrefix,
|
||||
'{{WorkbenchSeederNamespace}}' => $workbenchSeederNamespacePrefix,
|
||||
'{{ WorkbenchSeederNamespace }}' => $workbenchSeederNamespacePrefix,
|
||||
|
||||
'{{WorkbenchServiceProvider}}' => $serviceProvider,
|
||||
'{{ WorkbenchServiceProvider }}' => $serviceProvider,
|
||||
|
||||
'{{WorkbenchDatabaseSeeder}}' => $databaseSeeder,
|
||||
'{{ WorkbenchDatabaseSeeder }}' => $databaseSeeder,
|
||||
|
||||
'{{WorkbenchUserModel}}' => $userModel,
|
||||
'{{ WorkbenchUserModel }}' => $userModel,
|
||||
|
||||
'{{WorkbenchUserFactory}}' => $userFactory,
|
||||
'{{ WorkbenchUserFactory }}' => $userFactory,
|
||||
'Database\Factories\UserFactory' => $userFactory,
|
||||
'Orchestra\Testbench\Factories\UserFactory' => $userFactory,
|
||||
];
|
||||
|
||||
$this->filesystem->replaceInFile(array_keys($keywords), array_values($keywords), $filename);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\View\Compilers\BladeCompiler;
|
||||
|
||||
use function Orchestra\Sidekick\Filesystem\join_paths;
|
||||
|
||||
class AuthServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Bootstrap services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
$this->booted(function () {
|
||||
$this->loadRoutesFrom((string) realpath(join_paths(__DIR__, '..', 'routes', 'workbench-auth.php')));
|
||||
});
|
||||
|
||||
$this->loadViewsFrom((string) realpath(join_paths(__DIR__, '..', 'resources', 'views')), '');
|
||||
|
||||
$this->loadViewComponentsAs('', [
|
||||
View\Components\AppLayout::class,
|
||||
View\Components\GuestLayout::class,
|
||||
]);
|
||||
|
||||
$this->loadAnonymousComponentsFrom((string) realpath(join_paths(__DIR__, '..', 'resources', 'views', 'components')));
|
||||
|
||||
if ($this->app->runningInConsole()) {
|
||||
$this->publishes([
|
||||
__DIR__.'/../public/' => public_path(''),
|
||||
], ['laravel-assets']);
|
||||
}
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
#[\Override]
|
||||
protected function loadViewsFrom($path, $namespace)
|
||||
{
|
||||
if (empty($namespace)) {
|
||||
$this->callAfterResolving('view', static function ($view) use ($path) {
|
||||
$view->getFinder()->addLocation($path);
|
||||
});
|
||||
}
|
||||
|
||||
parent::loadViewsFrom($path, $namespace);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the given view components with a custom prefix.
|
||||
*/
|
||||
protected function loadAnonymousComponentsFrom(string $path, ?string $prefix = null): void
|
||||
{
|
||||
$this->callAfterResolving(BladeCompiler::class, static function ($blade) use ($path, $prefix) {
|
||||
$blade->anonymousComponentPath($path, $prefix);
|
||||
});
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class BuildParser
|
||||
{
|
||||
/**
|
||||
* List of disallowed commands.
|
||||
*/
|
||||
protected static array $disallowedCommands = [
|
||||
'workbench:build',
|
||||
'workbench:devtool',
|
||||
'workbench:install',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get Workbench build steps.
|
||||
*
|
||||
* @param array<int|string, array<string, mixed>|string> $config
|
||||
* @return \Illuminate\Support\Collection<string, array<string, mixed>>
|
||||
*/
|
||||
public static function make(array $config): Collection
|
||||
{
|
||||
return (new Collection($config))
|
||||
->map(static function (array|string $build) {
|
||||
/** @var string $name */
|
||||
$name = match (true) {
|
||||
\is_array($build) => array_key_first($build),
|
||||
\is_string($build) => $build,
|
||||
};
|
||||
|
||||
/** @var array<string, mixed> $options */
|
||||
$options = match (true) {
|
||||
\is_array($build) => array_shift($build),
|
||||
\is_string($build) => [],
|
||||
};
|
||||
|
||||
return [
|
||||
'name' => $name,
|
||||
'options' => (new Collection($options))->mapWithKeys(static fn ($value, $key) => [$key => $value])->all(),
|
||||
];
|
||||
})->whereNotIn(
|
||||
'name',
|
||||
(new Collection(static::$disallowedCommands))
|
||||
->transform(static fn ($command) => [$command, str_replace(':', '-', $command)])
|
||||
->flatten(),
|
||||
)->mapWithKeys(static fn (array $build) => [
|
||||
$build['name'] => $build['options'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench\Console;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Contracts\Console\Kernel as ConsoleKernel;
|
||||
use Illuminate\Support\Collection;
|
||||
use Orchestra\Workbench\BuildParser;
|
||||
use Orchestra\Workbench\Contracts\RecipeManager;
|
||||
use Orchestra\Workbench\Recipes\Command as CommandRecipe;
|
||||
use Orchestra\Workbench\Workbench;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
|
||||
/**
|
||||
* @phpstan-import-type TWorkbenchConfig from \Orchestra\Testbench\Foundation\Config
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
#[AsCommand(name: 'workbench:build', description: 'Run builds for workbench')]
|
||||
class BuildCommand extends Command
|
||||
{
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle(ConsoleKernel $kernel, RecipeManager $recipes)
|
||||
{
|
||||
$commands = (new Collection($kernel->all()))
|
||||
->keys()
|
||||
->filter(static fn ($command) => \is_string($command))
|
||||
->mapWithKeys(static fn (string $command) => [str_replace(':', '-', $command) => $command]);
|
||||
|
||||
/** @var array<int|string, array<string, mixed>|string> $build */
|
||||
$build = Workbench::config('build');
|
||||
|
||||
BuildParser::make($build)
|
||||
->each(function (array $options, string $name) use ($kernel, $recipes, $commands) {
|
||||
/** @var array<string, mixed> $options */
|
||||
if ($recipes->hasCommand($name)) {
|
||||
tap($recipes->command($name), static function ($recipe) use ($options) {
|
||||
if ($recipe instanceof CommandRecipe) {
|
||||
$recipe->options = $options;
|
||||
}
|
||||
})->handle($kernel, $this->output);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$command = $commands->get($name) ?? $commands->first(static fn ($commandName) => $name === $commandName);
|
||||
|
||||
if (! \is_null($command)) {
|
||||
$recipes->commandUsing($command, $options)->handle($kernel, $this->output);
|
||||
}
|
||||
});
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench\Console;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
|
||||
#[AsCommand(name: 'workbench:create-sqlite-db', description: 'Create sqlite database file')]
|
||||
class CreateSqliteDbCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'workbench:create-sqlite-db
|
||||
{--database=database.sqlite : Set the database name}
|
||||
{--force : Overwrite the database file}';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
return $this->call('package:create-sqlite-db', [
|
||||
'--database' => $this->option('database'),
|
||||
'--force' => $this->option('force'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench\Console;
|
||||
|
||||
use Composer\InstalledVersions;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Contracts\Console\PromptsForMissingInput;
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Orchestra\Testbench\Foundation\Console\Actions\EnsureDirectoryExists;
|
||||
use Orchestra\Testbench\Foundation\Console\Actions\GeneratesFile;
|
||||
use Orchestra\Workbench\Actions\DumpComposerAutoloads;
|
||||
use Orchestra\Workbench\Actions\ModifyComposer;
|
||||
use Orchestra\Workbench\Events\InstallEnded;
|
||||
use Orchestra\Workbench\Events\InstallStarted;
|
||||
use Orchestra\Workbench\StubRegistrar;
|
||||
use Orchestra\Workbench\Workbench;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
use function Laravel\Prompts\confirm;
|
||||
use function Orchestra\Sidekick\Filesystem\join_paths;
|
||||
use function Orchestra\Sidekick\is_testbench_cli;
|
||||
use function Orchestra\Testbench\package_path;
|
||||
|
||||
#[AsCommand(name: 'workbench:devtool', description: 'Configure Workbench for package development')]
|
||||
class DevToolCommand extends Command implements PromptsForMissingInput
|
||||
{
|
||||
/**
|
||||
* Namespace prefix for Workbench environment.
|
||||
*/
|
||||
protected string $workbenchNamespacePrefix = 'Workbench\\';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle(Filesystem $filesystem)
|
||||
{
|
||||
$workingPath = package_path();
|
||||
|
||||
event(new InstallStarted($this->input, $this->output, $this->components));
|
||||
|
||||
$this->prepareWorkbenchNamespaces($filesystem, $workingPath);
|
||||
$this->prepareWorkbenchDirectories($filesystem, $workingPath);
|
||||
|
||||
if ($this->option('install') === true) {
|
||||
$this->call('workbench:install', [
|
||||
'--force' => $this->option('force'),
|
||||
'--no-devtool' => true,
|
||||
'--basic' => $this->option('basic'),
|
||||
]);
|
||||
}
|
||||
|
||||
return tap(Command::SUCCESS, function ($exitCode) use ($workingPath) {
|
||||
event(new InstallEnded($this->input, $this->output, $this->components, $exitCode));
|
||||
|
||||
(new DumpComposerAutoloads($workingPath))->handle();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare workbench directories.
|
||||
*/
|
||||
protected function prepareWorkbenchDirectories(Filesystem $filesystem, string $workingPath): void
|
||||
{
|
||||
$workbenchWorkingPath = join_paths($workingPath, 'workbench');
|
||||
|
||||
(new EnsureDirectoryExists(
|
||||
filesystem: $filesystem,
|
||||
components: $this->components,
|
||||
))->handle(
|
||||
(new Collection([
|
||||
join_paths('app', 'Models'),
|
||||
|
||||
join_paths('database', 'factories'),
|
||||
join_paths('database', 'migrations'),
|
||||
join_paths('database', 'seeders'),
|
||||
]))->when(
|
||||
$this->option('basic') === false,
|
||||
fn ($directories) => $directories->push(...['bootstrap', 'routes', join_paths('resources', 'views')])
|
||||
)->map(static fn ($directory) => join_paths($workbenchWorkingPath, $directory))
|
||||
);
|
||||
|
||||
$this->callSilently('make:provider', [
|
||||
'name' => 'WorkbenchServiceProvider',
|
||||
'--preset' => 'workbench',
|
||||
'--force' => (bool) $this->option('force'),
|
||||
]);
|
||||
|
||||
StubRegistrar::replaceInFile($filesystem, join_paths($workbenchWorkingPath, 'Providers', 'WorkbenchServiceProvider.php'));
|
||||
|
||||
$this->prepareWorkbenchDatabaseSchema($filesystem, $workbenchWorkingPath);
|
||||
|
||||
if ($this->option('basic') === false) {
|
||||
foreach (['console', 'web'] as $route) {
|
||||
(new GeneratesFile(
|
||||
filesystem: $filesystem,
|
||||
components: $this->components,
|
||||
force: (bool) $this->option('force'),
|
||||
))->handle(
|
||||
(string) Workbench::stubFile("routes.{$route}"),
|
||||
join_paths($workbenchWorkingPath, 'routes', "{$route}.php")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare workbench namespace to `composer.json`.
|
||||
*/
|
||||
protected function prepareWorkbenchNamespaces(Filesystem $filesystem, string $workingPath): void
|
||||
{
|
||||
(new ModifyComposer($workingPath))
|
||||
->handle(fn (array $content) => $this->appendScriptsToComposer(
|
||||
$this->appendAutoloadDevToComposer($content, $filesystem), $filesystem
|
||||
));
|
||||
|
||||
Workbench::flushCachedClassAndNamespaces();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare workbench database schema including user model, factory and seeder.
|
||||
*/
|
||||
protected function prepareWorkbenchDatabaseSchema(Filesystem $filesystem, string $workingPath): void
|
||||
{
|
||||
$this->callSilently('make:user-model', [
|
||||
'--preset' => 'workbench',
|
||||
'--force' => (bool) $this->option('force'),
|
||||
]);
|
||||
|
||||
StubRegistrar::replaceInFile($filesystem, join_paths($workingPath, 'app', 'Models', 'User.php'));
|
||||
|
||||
$this->callSilently('make:user-factory', [
|
||||
'--preset' => 'workbench',
|
||||
'--force' => (bool) $this->option('force'),
|
||||
]);
|
||||
|
||||
StubRegistrar::replaceInFile($filesystem, join_paths($workingPath, 'database', 'factories', 'UserFactory.php'));
|
||||
|
||||
(new GeneratesFile(
|
||||
filesystem: $filesystem,
|
||||
components: $this->components,
|
||||
force: (bool) $this->option('force'),
|
||||
))->handle(
|
||||
(string) Workbench::stubFile('seeders.database'),
|
||||
join_paths($workingPath, 'database', 'seeders', 'DatabaseSeeder.php')
|
||||
);
|
||||
|
||||
StubRegistrar::replaceInFile($filesystem, join_paths($workingPath, 'database', 'seeders', 'DatabaseSeeder.php'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Append `scripts` to `composer.json`.
|
||||
*/
|
||||
protected function appendScriptsToComposer(array $content, Filesystem $filesystem): array
|
||||
{
|
||||
$hasScriptsSection = \array_key_exists('scripts', $content);
|
||||
$hasTestbenchDusk = InstalledVersions::isInstalled('orchestra/testbench-dusk');
|
||||
|
||||
if (! $hasScriptsSection) {
|
||||
$content['scripts'] = [];
|
||||
}
|
||||
|
||||
$postAutoloadDumpScripts = array_filter([
|
||||
'@clear',
|
||||
'@prepare',
|
||||
$hasTestbenchDusk ? '@dusk:install-chromedriver' : null,
|
||||
]);
|
||||
|
||||
if (! \array_key_exists('post-autoload-dump', $content['scripts'])) {
|
||||
$content['scripts']['post-autoload-dump'] = $postAutoloadDumpScripts;
|
||||
} else {
|
||||
$content['scripts']['post-autoload-dump'] = array_values(array_unique([
|
||||
...$postAutoloadDumpScripts,
|
||||
...Arr::wrap($content['scripts']['post-autoload-dump']),
|
||||
]));
|
||||
}
|
||||
|
||||
$content['scripts']['clear'] = '@php vendor/bin/testbench package:purge-skeleton --ansi';
|
||||
$content['scripts']['prepare'] = '@php vendor/bin/testbench package:discover --ansi';
|
||||
|
||||
if ($hasTestbenchDusk) {
|
||||
$content['scripts']['dusk:install-chromedriver'] = '@php vendor/bin/dusk-updater detect --auto-update --ansi';
|
||||
}
|
||||
|
||||
$content['scripts']['build'] = '@php vendor/bin/testbench workbench:build --ansi';
|
||||
$content['scripts']['serve'] = [
|
||||
'Composer\\Config::disableProcessTimeout',
|
||||
'@build',
|
||||
$hasTestbenchDusk && is_testbench_cli(dusk: true)
|
||||
? '@php vendor/bin/testbench-dusk serve --ansi'
|
||||
: '@php vendor/bin/testbench serve --ansi',
|
||||
];
|
||||
|
||||
if (! \array_key_exists('lint', $content['scripts'])) {
|
||||
$lintScripts = [];
|
||||
|
||||
if (InstalledVersions::isInstalled('laravel/pint')) {
|
||||
$lintScripts[] = '@php vendor/bin/pint --ansi';
|
||||
} elseif ($filesystem->isFile(Workbench::packagePath('pint.json'))) {
|
||||
$lintScripts[] = 'pint';
|
||||
}
|
||||
|
||||
if (InstalledVersions::isInstalled('phpstan/phpstan')) {
|
||||
$lintScripts[] = '@php vendor/bin/phpstan analyse --verbose --ansi';
|
||||
}
|
||||
|
||||
if (\count($lintScripts) > 0) {
|
||||
$content['scripts']['lint'] = $lintScripts;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
$filesystem->isFile(Workbench::packagePath('phpunit.xml'))
|
||||
|| $filesystem->isFile(Workbench::packagePath('phpunit.xml.dist'))
|
||||
) {
|
||||
if (! \array_key_exists('test', $content['scripts'])) {
|
||||
$content['scripts']['test'] = [
|
||||
'@clear',
|
||||
InstalledVersions::isInstalled('pestphp/pest')
|
||||
? '@php vendor/bin/pest'
|
||||
: '@php vendor/bin/phpunit',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append `autoload-dev` to `composer.json`.
|
||||
*/
|
||||
protected function appendAutoloadDevToComposer(array $content, Filesystem $filesystem): array
|
||||
{
|
||||
/** @var array{autoload-dev?: array{psr-4?: array<string, string>}} $content */
|
||||
if (! \array_key_exists('autoload-dev', $content)) {
|
||||
$content['autoload-dev'] = [];
|
||||
}
|
||||
|
||||
/** @var array{autoload-dev: array{psr-4?: array<string, string>}} $content */
|
||||
if (! \array_key_exists('psr-4', $content['autoload-dev'])) {
|
||||
$content['autoload-dev']['psr-4'] = [];
|
||||
}
|
||||
|
||||
if (confirm('Prefix with `Workbench` namespace?', default: true) === false) {
|
||||
$this->workbenchNamespacePrefix = '';
|
||||
}
|
||||
|
||||
$namespaces = [
|
||||
'workbench/app/' => $this->workbenchNamespacePrefix.'App\\',
|
||||
'workbench/database/factories/' => $this->workbenchNamespacePrefix.'Database\\Factories\\',
|
||||
'workbench/database/seeders/' => $this->workbenchNamespacePrefix.'Database\\Seeders\\',
|
||||
];
|
||||
|
||||
$autoloads = array_flip($content['autoload-dev']['psr-4']);
|
||||
|
||||
foreach ($namespaces as $path => $namespace) {
|
||||
if (! \array_key_exists($path, $autoloads)) {
|
||||
$content['autoload-dev']['psr-4'][$namespace] = $path;
|
||||
|
||||
$this->components->task(\sprintf(
|
||||
'Added [%s] for [%s] to Composer', $namespace, './'.rtrim($path, '/')
|
||||
));
|
||||
} else {
|
||||
$this->components->twoColumnDetail(
|
||||
\sprintf('Composer already contains [%s] path assigned to [%s] namespace', './'.rtrim($path, '/'), $autoloads[$path]),
|
||||
'<fg=yellow;options=bold>SKIPPED</>'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt the user for any missing arguments.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function promptForMissingArguments(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$install = null;
|
||||
|
||||
if (\is_null($input->getOption('install'))) {
|
||||
$install = confirm('Run Workbench installation?', true);
|
||||
}
|
||||
|
||||
if (! \is_null($install)) {
|
||||
$input->setOption('install', $install);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the console command options.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getOptions()
|
||||
{
|
||||
return [
|
||||
['force', 'f', InputOption::VALUE_NONE, 'Overwrite any existing files'],
|
||||
['install', null, InputOption::VALUE_NEGATABLE, 'Run Workbench installation'],
|
||||
['basic', null, InputOption::VALUE_NONE, 'Workbench installation without discovers and routes'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench\Console;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
|
||||
#[AsCommand(name: 'workbench:drop-sqlite-db', description: 'Drop sqlite database file')]
|
||||
class DropSqliteDbCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'workbench:drop-sqlite-db
|
||||
{--database=database.sqlite : Set the database name}
|
||||
{--all : Delete all SQLite databases}';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
return $this->call('package:drop-sqlite-db', [
|
||||
'--database' => $this->option('database'),
|
||||
'--all' => $this->option('all'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench\Console;
|
||||
|
||||
use Composer\InstalledVersions;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Contracts\Console\PromptsForMissingInput;
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
use Illuminate\Support\Collection;
|
||||
use Orchestra\Testbench\Foundation\Console\Actions\GeneratesFile;
|
||||
use Orchestra\Workbench\StubRegistrar;
|
||||
use Orchestra\Workbench\Workbench;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
use function Laravel\Prompts\confirm;
|
||||
use function Laravel\Prompts\select;
|
||||
use function Orchestra\Sidekick\Filesystem\join_paths;
|
||||
use function Orchestra\Testbench\package_path;
|
||||
|
||||
#[AsCommand(name: 'workbench:install', description: 'Setup Workbench for package development')]
|
||||
class InstallCommand extends Command implements PromptsForMissingInput
|
||||
{
|
||||
/**
|
||||
* The `testbench.yaml` default configuration file.
|
||||
*/
|
||||
public static ?string $configurationBaseFile = null;
|
||||
|
||||
/**
|
||||
* Determine if Package also uses Testbench Dusk.
|
||||
*/
|
||||
protected ?bool $hasTestbenchDusk = null;
|
||||
|
||||
/** {@inheritDoc} */
|
||||
#[\Override]
|
||||
protected function initialize(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$this->hasTestbenchDusk = InstalledVersions::isInstalled('orchestra/testbench-dusk');
|
||||
|
||||
parent::initialize($input, $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle(Filesystem $filesystem)
|
||||
{
|
||||
$devtool = match (true) {
|
||||
\is_bool($this->option('devtool')) => $this->option('devtool'),
|
||||
default => $this->components->confirm('Install Workbench DevTool?', true),
|
||||
};
|
||||
|
||||
if ($devtool === true) {
|
||||
$this->call('workbench:devtool', [
|
||||
'--force' => $this->option('force'),
|
||||
'--no-install' => true,
|
||||
'--basic' => $this->option('basic'),
|
||||
]);
|
||||
}
|
||||
|
||||
$workingPath = package_path();
|
||||
|
||||
$this->copyTestbenchConfigurationFile($filesystem, $workingPath);
|
||||
$this->copyTestbenchDotEnvFile($filesystem, $workingPath);
|
||||
$this->prepareWorkbenchDirectories($filesystem, $workingPath);
|
||||
|
||||
$this->replaceDefaultLaravelSkeletonInTestbenchConfigurationFile($filesystem, $workingPath);
|
||||
|
||||
$this->call('workbench:create-sqlite-db', ['--force' => true]);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare workbench directories.
|
||||
*/
|
||||
protected function prepareWorkbenchDirectories(Filesystem $filesystem, string $workingPath): void
|
||||
{
|
||||
if (! $this->input->isInteractive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$workbenchWorkingPath = join_paths($workingPath, 'workbench');
|
||||
|
||||
foreach (['app' => true, 'providers' => false] as $bootstrap => $default) {
|
||||
if (! confirm("Generate `workbench/bootstrap/{$bootstrap}.php` file?", default: $default)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
(new GeneratesFile(
|
||||
filesystem: $filesystem,
|
||||
components: $this->components,
|
||||
force: (bool) $this->option('force'),
|
||||
))->handle(
|
||||
(string) realpath(join_paths(__DIR__, 'stubs', 'bootstrap', "{$bootstrap}.php")),
|
||||
join_paths($workbenchWorkingPath, 'bootstrap', "{$bootstrap}.php")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the "testbench.yaml" file.
|
||||
*/
|
||||
protected function copyTestbenchConfigurationFile(Filesystem $filesystem, string $workingPath): void
|
||||
{
|
||||
$from = ! \is_null(static::$configurationBaseFile)
|
||||
? (string) realpath(static::$configurationBaseFile)
|
||||
: (string) Workbench::stubFile($this->option('basic') === true ? 'config.basic' : 'config');
|
||||
|
||||
$to = join_paths($workingPath, 'testbench.yaml');
|
||||
|
||||
(new GeneratesFile(
|
||||
filesystem: $filesystem,
|
||||
components: $this->components,
|
||||
force: (bool) $this->option('force'),
|
||||
))->handle($from, $to);
|
||||
|
||||
StubRegistrar::replaceInFile($filesystem, $to);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the ".env" file.
|
||||
*/
|
||||
protected function copyTestbenchDotEnvFile(Filesystem $filesystem, string $workingPath): void
|
||||
{
|
||||
$workbenchWorkingPath = join_paths($workingPath, 'workbench');
|
||||
|
||||
$from = $this->laravel->basePath('.env.example');
|
||||
|
||||
if (! $filesystem->isFile($this->laravel->basePath('.env.example'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var \Illuminate\Support\Collection<int, string> $choices */
|
||||
$choices = (new Collection($this->environmentFiles()))
|
||||
->reject(static fn ($file) => $filesystem->isFile(join_paths($workbenchWorkingPath, $file)))
|
||||
->values();
|
||||
|
||||
if (! $this->option('force') && $choices->isEmpty()) {
|
||||
$this->components->twoColumnDetail(
|
||||
'File [.env] already exists', '<fg=yellow;options=bold>SKIPPED</>'
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var string|null $targetEnvironmentFile */
|
||||
$targetEnvironmentFile = $this->input->isInteractive()
|
||||
? select(
|
||||
"Export '.env' file as?",
|
||||
$choices->prepend('Skip exporting .env'), // @phpstan-ignore argument.type
|
||||
) : null;
|
||||
|
||||
if (\in_array($targetEnvironmentFile, [null, 'Skip exporting .env'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$filesystem->ensureDirectoryExists($workbenchWorkingPath);
|
||||
|
||||
$this->generateSeparateEnvironmentFileForTestbenchDusk($filesystem, $workbenchWorkingPath, $targetEnvironmentFile);
|
||||
|
||||
(new GeneratesFile(
|
||||
filesystem: $filesystem,
|
||||
components: $this->components,
|
||||
force: (bool) $this->option('force'),
|
||||
))->handle(
|
||||
$from,
|
||||
join_paths($workbenchWorkingPath, $targetEnvironmentFile)
|
||||
);
|
||||
|
||||
(new GeneratesFile(
|
||||
filesystem: $filesystem,
|
||||
force: (bool) $this->option('force'),
|
||||
))->handle(
|
||||
(string) Workbench::stubFile('gitignore'),
|
||||
join_paths($workbenchWorkingPath, '.gitignore')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the default `laravel` skeleton for Testbench Dusk.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
protected function replaceDefaultLaravelSkeletonInTestbenchConfigurationFile(Filesystem $filesystem, string $workingPath): void
|
||||
{
|
||||
if ($this->hasTestbenchDusk === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$filesystem->replaceInFile(["laravel: '@testbench'"], ["laravel: '@testbench-dusk'"], join_paths($workingPath, 'testbench.yaml'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate separate `.env.dusk` equivalent for Testbench Dusk.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
protected function generateSeparateEnvironmentFileForTestbenchDusk(Filesystem $filesystem, string $workbenchWorkingPath, string $targetEnvironmentFile): void
|
||||
{
|
||||
if ($this->hasTestbenchDusk === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->components->confirm('Create separate environment file for Testbench Dusk?', false)) {
|
||||
(new GeneratesFile(
|
||||
filesystem: $filesystem,
|
||||
components: $this->components,
|
||||
force: (bool) $this->option('force'),
|
||||
))->handle(
|
||||
$this->laravel->basePath('.env.example'),
|
||||
join_paths($workbenchWorkingPath, str_replace('.env', '.env.dusk', $targetEnvironmentFile))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get possible environment files.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
protected function environmentFiles(): array
|
||||
{
|
||||
return [
|
||||
'.env',
|
||||
'.env.example',
|
||||
'.env.dist',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt the user for any missing arguments.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function promptForMissingArguments(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$devtool = null;
|
||||
|
||||
if (\is_null($input->getOption('devtool'))) {
|
||||
$devtool = confirm('Run Workbench DevTool installation?', true);
|
||||
}
|
||||
|
||||
if (! \is_null($devtool)) {
|
||||
$input->setOption('devtool', $devtool);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the console command options.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getOptions()
|
||||
{
|
||||
return [
|
||||
['force', 'f', InputOption::VALUE_NONE, 'Overwrite any existing files'],
|
||||
['devtool', null, InputOption::VALUE_NEGATABLE, 'Run DevTool installation'],
|
||||
['basic', null, InputOption::VALUE_NONE, 'Skipped routes and discovers installation'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench\Console;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
#[AsCommand(name: 'workbench:purge-skeleton', description: 'Purge skeleton folder to original state')]
|
||||
class PurgeSkeletonCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'workbench:purge-skeleton';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
return $this->call('package:purge-skeleton');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench\Console;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
#[AsCommand(name: 'workbench:sync-skeleton', description: 'Sync skeleton folder to be served externally')]
|
||||
class SyncSkeletonCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'workbench:sync-skeleton';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
return $this->call('package:sync-skeleton');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
|
||||
use function Orchestra\Testbench\default_skeleton_path;
|
||||
|
||||
return Application::configure(basePath: $APP_BASE_PATH ?? default_skeleton_path())
|
||||
->withRouting(
|
||||
web: __DIR__.'/../routes/web.php',
|
||||
commands: __DIR__.'/../routes/console.php',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
//
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
//
|
||||
})->create();
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
//
|
||||
];
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Workbench\Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Orchestra\Testbench\Factories\UserFactory;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
{
|
||||
use WithoutModelEvents;
|
||||
|
||||
/**
|
||||
* Seed the application's database.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// UserFactory::new()->times(10)->create();
|
||||
|
||||
UserFactory::new()->create([
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Inspiring;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
// Artisan::command('inspire', function () {
|
||||
// $this->comment(Inspiring::quote());
|
||||
// })->purpose('Display an inspiring quote');
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/', function () {
|
||||
return view('welcome');
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
laravel: '@testbench'
|
||||
|
||||
providers:
|
||||
# - {{ WorkbenchServiceProvider }}
|
||||
|
||||
migrations:
|
||||
- workbench/database/migrations
|
||||
|
||||
seeders:
|
||||
- {{ WorkbenchDatabaseSeeder }}
|
||||
|
||||
workbench:
|
||||
install: true
|
||||
@@ -0,0 +1,33 @@
|
||||
laravel: '@testbench'
|
||||
|
||||
providers:
|
||||
# - {{ WorkbenchServiceProvider }}
|
||||
|
||||
migrations:
|
||||
- workbench/database/migrations
|
||||
|
||||
seeders:
|
||||
- {{ WorkbenchDatabaseSeeder }}
|
||||
|
||||
workbench:
|
||||
start: '/'
|
||||
install: true
|
||||
health: false
|
||||
discovers:
|
||||
web: true
|
||||
api: true
|
||||
commands: true
|
||||
components: false
|
||||
factories: true
|
||||
views: false
|
||||
build:
|
||||
- asset-publish
|
||||
- create-sqlite-db
|
||||
- db-wipe
|
||||
- migrate-fresh
|
||||
assets:
|
||||
- laravel-assets
|
||||
sync:
|
||||
- from: storage
|
||||
to: workbench/storage
|
||||
reverse: true
|
||||
@@ -0,0 +1,3 @@
|
||||
/storage
|
||||
.env
|
||||
.env.dusk
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench\Contracts;
|
||||
|
||||
use Illuminate\Contracts\Console\Kernel as ConsoleKernel;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
interface Recipe
|
||||
{
|
||||
/**
|
||||
* Run the recipe.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle(ConsoleKernel $kernel, OutputInterface $output);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench\Contracts;
|
||||
|
||||
interface RecipeManager
|
||||
{
|
||||
/**
|
||||
* Create anonymous command driver.
|
||||
*
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
public function commandUsing(string $command, array $options = []): Recipe;
|
||||
|
||||
/**
|
||||
* Run the recipe by name.
|
||||
*/
|
||||
public function command(string $driver): Recipe;
|
||||
|
||||
/**
|
||||
* Determine recipe is available by name.
|
||||
*/
|
||||
public function hasCommand(string $driver): bool;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench\Events\Concerns;
|
||||
|
||||
trait InteractsWithInput
|
||||
{
|
||||
/**
|
||||
* Determine if event executed with `--basic` option.
|
||||
*/
|
||||
public function isBasicInstallation(): bool
|
||||
{
|
||||
return $this->input->hasOption('basic') && $this->input->getOption('basic') === true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench\Events;
|
||||
|
||||
use Illuminate\Console\View\Components\Factory;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
readonly class InstallEnded
|
||||
{
|
||||
use Concerns\InteractsWithInput;
|
||||
|
||||
/**
|
||||
* Construct a new event.
|
||||
*/
|
||||
public function __construct(
|
||||
public InputInterface $input,
|
||||
public OutputInterface $output,
|
||||
public Factory $components,
|
||||
public int $exitCode
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench\Events;
|
||||
|
||||
use Illuminate\Console\View\Components\Factory;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
readonly class InstallStarted
|
||||
{
|
||||
use Concerns\InteractsWithInput;
|
||||
|
||||
/**
|
||||
* Construct a new event.
|
||||
*/
|
||||
public function __construct(
|
||||
public InputInterface $input,
|
||||
public OutputInterface $output,
|
||||
public Factory $components
|
||||
) {}
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench;
|
||||
|
||||
use BadMethodCallException;
|
||||
use Orchestra\Canvas\Core\Presets\Preset;
|
||||
|
||||
use function Orchestra\Sidekick\Filesystem\join_paths;
|
||||
|
||||
class GeneratorPreset extends Preset
|
||||
{
|
||||
/**
|
||||
* Preset name.
|
||||
*/
|
||||
public function name(): string
|
||||
{
|
||||
return 'workbench';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the base working directory.
|
||||
*/
|
||||
public function basePath(): string
|
||||
{
|
||||
return rtrim(Workbench::path(), DIRECTORY_SEPARATOR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the source directory.
|
||||
*/
|
||||
public function sourcePath(): string
|
||||
{
|
||||
return rtrim(Workbench::path('app'), DIRECTORY_SEPARATOR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the testing directory.
|
||||
*
|
||||
* @throws \BadMethodCallException
|
||||
*/
|
||||
public function testingPath(): never
|
||||
{
|
||||
throw new BadMethodCallException('Generating test is not supported for [workbench] preset');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the resource directory.
|
||||
*/
|
||||
public function resourcePath(): string
|
||||
{
|
||||
return rtrim(Workbench::path('resources'), DIRECTORY_SEPARATOR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the view directory.
|
||||
*/
|
||||
public function viewPath(): string
|
||||
{
|
||||
return rtrim(Workbench::path(join_paths('resources', 'views')), DIRECTORY_SEPARATOR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the factory directory.
|
||||
*/
|
||||
public function factoryPath(): string
|
||||
{
|
||||
return rtrim(Workbench::path(join_paths('database', 'factories')), DIRECTORY_SEPARATOR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the migration directory.
|
||||
*/
|
||||
public function migrationPath(): string
|
||||
{
|
||||
return rtrim(Workbench::path(join_paths('database', 'migrations')), DIRECTORY_SEPARATOR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the seeder directory.
|
||||
*/
|
||||
public function seederPath(): string
|
||||
{
|
||||
return rtrim(Workbench::path(join_paths('database', 'seeders')), DIRECTORY_SEPARATOR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Preset namespace.
|
||||
*/
|
||||
public function rootNamespace(): string
|
||||
{
|
||||
return Workbench::detectNamespace('app') ?? "Workbench\App\\";
|
||||
}
|
||||
|
||||
/**
|
||||
* Command namespace.
|
||||
*/
|
||||
public function commandNamespace(): string
|
||||
{
|
||||
return "{$this->rootNamespace()}Console\\";
|
||||
}
|
||||
|
||||
/**
|
||||
* Model namespace.
|
||||
*/
|
||||
public function modelNamespace(): string
|
||||
{
|
||||
return "{$this->rootNamespace()}Models\\";
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider namespace.
|
||||
*/
|
||||
public function providerNamespace(): string
|
||||
{
|
||||
return "{$this->rootNamespace()}Providers\\";
|
||||
}
|
||||
|
||||
/**
|
||||
* Database factory namespace.
|
||||
*/
|
||||
public function factoryNamespace(): string
|
||||
{
|
||||
return Workbench::detectNamespace('database/factories') ?? "Workbench\Database\Factories\\";
|
||||
}
|
||||
|
||||
/**
|
||||
* Database seeder namespace.
|
||||
*/
|
||||
public function seederNamespace(): string
|
||||
{
|
||||
return Workbench::detectNamespace('database/seeders') ?? "Workbench\Database\Seeders\\";
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing namespace.
|
||||
*
|
||||
* @throws \BadMethodCallException
|
||||
*/
|
||||
public function testingNamespace(): never
|
||||
{
|
||||
throw new BadMethodCallException('Generating test is not supported for [workbench] preset');
|
||||
}
|
||||
|
||||
/**
|
||||
* Preset has custom stub path.
|
||||
*/
|
||||
public function hasCustomStubPath(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench\Http\Controllers\Auth;
|
||||
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\View\View;
|
||||
use Orchestra\Workbench\Http\Controllers\Controller;
|
||||
use Orchestra\Workbench\Http\Requests\Auth\LoginRequest;
|
||||
|
||||
class AuthenticatedSessionController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the login view.
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
return view('auth.login');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming authentication request.
|
||||
*/
|
||||
public function store(LoginRequest $request): RedirectResponse
|
||||
{
|
||||
$request->authenticate();
|
||||
|
||||
$request->session()->regenerate();
|
||||
|
||||
return redirect()->intended($this->redirectToAfterLoggedIn());
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy an authenticated session.
|
||||
*/
|
||||
public function destroy(Request $request): RedirectResponse
|
||||
{
|
||||
Auth::guard('web')->logout();
|
||||
|
||||
$request->session()->invalidate();
|
||||
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect('/');
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench\Http\Controllers\Auth;
|
||||
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\View\View;
|
||||
use Orchestra\Workbench\Http\Controllers\Controller;
|
||||
|
||||
class ConfirmablePasswordController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show the confirm password view.
|
||||
*/
|
||||
public function show(): View
|
||||
{
|
||||
return view('auth.confirm-password');
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm the user's password.
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
if (! Auth::guard('web')->validate([
|
||||
'email' => $request->user()->email,
|
||||
'password' => $request->password,
|
||||
])) {
|
||||
throw ValidationException::withMessages([
|
||||
'password' => __('auth.password'),
|
||||
]);
|
||||
}
|
||||
|
||||
$request->session()->put('auth.password_confirmed_at', time());
|
||||
|
||||
return redirect()->intended(route('dashboard', absolute: false));
|
||||
}
|
||||
}
|
||||
Vendored
+24
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench\Http\Controllers\Auth;
|
||||
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Orchestra\Workbench\Http\Controllers\Controller;
|
||||
|
||||
class EmailVerificationNotificationController extends Controller
|
||||
{
|
||||
/**
|
||||
* Send a new email verification notification.
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
if ($request->user()->hasVerifiedEmail()) {
|
||||
return redirect()->intended(route('dashboard', absolute: false));
|
||||
}
|
||||
|
||||
$request->user()->sendEmailVerificationNotification();
|
||||
|
||||
return back()->with('status', 'verification-link-sent');
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench\Http\Controllers\Auth;
|
||||
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Orchestra\Workbench\Http\Controllers\Controller;
|
||||
|
||||
class EmailVerificationPromptController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the email verification prompt.
|
||||
*/
|
||||
public function __invoke(Request $request): RedirectResponse|View
|
||||
{
|
||||
return $request->user()->hasVerifiedEmail()
|
||||
? redirect()->intended(route('dashboard', absolute: false))
|
||||
: view('auth.verify-email');
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench\Http\Controllers\Auth;
|
||||
|
||||
use Illuminate\Auth\Events\PasswordReset;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rules;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\View\View;
|
||||
use Orchestra\Workbench\Http\Controllers\Controller;
|
||||
|
||||
class NewPasswordController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the password reset view.
|
||||
*/
|
||||
public function create(Request $request): View
|
||||
{
|
||||
return view('auth.reset-password', ['request' => $request]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming new password request.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'token' => ['required'],
|
||||
'email' => ['required', 'email'],
|
||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||
]);
|
||||
|
||||
// Here we will attempt to reset the user's password. If it is successful we
|
||||
// will update the password on an actual user model and persist it to the
|
||||
// database. Otherwise we will parse the error and return the response.
|
||||
$status = Password::reset(
|
||||
$request->only('email', 'password', 'password_confirmation', 'token'),
|
||||
function ($user) use ($request) {
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($request->password),
|
||||
'remember_token' => Str::random(60),
|
||||
])->save();
|
||||
|
||||
event(new PasswordReset($user));
|
||||
}
|
||||
);
|
||||
|
||||
// If the password was successfully reset, we will redirect the user back to
|
||||
// the application's home authenticated view. If there is an error we can
|
||||
// redirect them back to where they came from with their error message.
|
||||
return $status == Password::PASSWORD_RESET
|
||||
? redirect()->route('login')->with('status', __($status))
|
||||
: back()->withInput($request->only('email'))
|
||||
->withErrors(['email' => __($status)]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench\Http\Controllers\Auth;
|
||||
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Orchestra\Workbench\Http\Controllers\Controller;
|
||||
|
||||
class PasswordController extends Controller
|
||||
{
|
||||
/**
|
||||
* Update the user's password.
|
||||
*/
|
||||
public function update(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validateWithBag('updatePassword', [
|
||||
'current_password' => ['required', 'current_password'],
|
||||
'password' => ['required', Password::defaults(), 'confirmed'],
|
||||
]);
|
||||
|
||||
$request->user()->update([
|
||||
'password' => Hash::make($validated['password']),
|
||||
]);
|
||||
|
||||
return back()->with('status', 'password-updated');
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench\Http\Controllers\Auth;
|
||||
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\View\View;
|
||||
use Orchestra\Workbench\Http\Controllers\Controller;
|
||||
|
||||
class PasswordResetLinkController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the password reset link request view.
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
return view('auth.forgot-password');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming password reset link request.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'email' => ['required', 'email'],
|
||||
]);
|
||||
|
||||
// We will send the password reset link to this user. Once we have attempted
|
||||
// to send the link, we will examine the response then see the message we
|
||||
// need to show to the user. Finally, we'll send out a proper response.
|
||||
$status = Password::sendResetLink(
|
||||
$request->only('email')
|
||||
);
|
||||
|
||||
return $status == Password::RESET_LINK_SENT
|
||||
? back()->with('status', __($status))
|
||||
: back()->withInput($request->only('email'))
|
||||
->withErrors(['email' => __($status)]);
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench\Http\Controllers\Auth;
|
||||
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Foundation\Auth\User;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\View\View;
|
||||
use Orchestra\Sidekick\Env;
|
||||
use Orchestra\Workbench\Http\Controllers\Controller;
|
||||
|
||||
class RegisteredUserController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the registration view.
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
return view('auth.register');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming registration request.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
|
||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||
]);
|
||||
|
||||
$userModel = Env::get('TESTBENCH_USER_MODEL', User::class);
|
||||
|
||||
$user = $userModel::forceCreate([
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
'password' => Hash::make($request->password),
|
||||
]);
|
||||
|
||||
event(new Registered($user));
|
||||
|
||||
Auth::login($user);
|
||||
|
||||
return redirect($this->redirectToAfterLoggedIn());
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench\Http\Controllers\Auth;
|
||||
|
||||
use Illuminate\Auth\Events\Verified;
|
||||
use Illuminate\Foundation\Auth\EmailVerificationRequest;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Orchestra\Workbench\Http\Controllers\Controller;
|
||||
|
||||
class VerifyEmailController extends Controller
|
||||
{
|
||||
/**
|
||||
* Mark the authenticated user's email address as verified.
|
||||
*/
|
||||
public function __invoke(EmailVerificationRequest $request): RedirectResponse
|
||||
{
|
||||
if ($request->user()->hasVerifiedEmail()) {
|
||||
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
||||
}
|
||||
|
||||
if ($request->user()->markEmailAsVerified()) {
|
||||
event(new Verified($request->user()));
|
||||
}
|
||||
|
||||
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench\Http\Controllers;
|
||||
|
||||
use Orchestra\Workbench\Workbench;
|
||||
|
||||
abstract class Controller extends \Illuminate\Routing\Controller
|
||||
{
|
||||
/**
|
||||
* Get redirect to path after logged in.
|
||||
*/
|
||||
protected function redirectToAfterLoggedIn(): ?string
|
||||
{
|
||||
$start = Workbench::config('start') ?? '/';
|
||||
$hasAuthentication = Workbench::config('auth') ?? false;
|
||||
|
||||
return match (true) {
|
||||
$hasAuthentication === true && $start === '/' => route('dashboard', absolute: false),
|
||||
$hasAuthentication === true => $start,
|
||||
default => $start,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Redirect;
|
||||
use Illuminate\View\View;
|
||||
use Orchestra\Workbench\Http\Requests\ProfileUpdateRequest;
|
||||
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the user's profile form.
|
||||
*/
|
||||
public function edit(Request $request): View
|
||||
{
|
||||
return view('profile.edit', [
|
||||
'user' => $request->user(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 Redirect::route('profile.edit')->with('status', 'profile-updated');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the user's account.
|
||||
*/
|
||||
public function destroy(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validateWithBag('userDeletion', [
|
||||
'password' => ['required', 'current_password'],
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
Auth::logout();
|
||||
|
||||
$user->delete();
|
||||
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return Redirect::to('/');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use Illuminate\Support\Str;
|
||||
use Orchestra\Workbench\Workbench;
|
||||
|
||||
class WorkbenchController extends Controller
|
||||
{
|
||||
/**
|
||||
* Start page.
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function start(Request $request)
|
||||
{
|
||||
$workbench = Workbench::config();
|
||||
|
||||
if (\is_null($workbench['user'])) {
|
||||
return $this->logout($workbench['guard']);
|
||||
}
|
||||
|
||||
$request->session()->flush();
|
||||
|
||||
return $this->login((string) $workbench['user'], $workbench['guard']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the authenticated user identifier and class name.
|
||||
*
|
||||
* @param string|null $guard
|
||||
* @return array<string, mixed>
|
||||
*
|
||||
* @phpstan-return array{id?: string|int, className?: string}
|
||||
*/
|
||||
public function user($guard = null)
|
||||
{
|
||||
$user = Auth::guard($guard)->user();
|
||||
|
||||
if (! $user) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $user->getAuthIdentifier(),
|
||||
'className' => $user::class,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Login using the given user ID / email.
|
||||
*
|
||||
* @param string $userId
|
||||
* @param string|null $guard
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function login($userId, $guard = null)
|
||||
{
|
||||
$guard = $guard ?: config('auth.defaults.guard');
|
||||
|
||||
/** @var \Illuminate\Contracts\Auth\UserProvider $provider */
|
||||
$provider = Auth::guard($guard)->getProvider(); // @phpstan-ignore method.notFound
|
||||
|
||||
$user = Str::contains($userId, '@')
|
||||
? $provider->retrieveByCredentials(['email' => $userId])
|
||||
: $provider->retrieveById($userId);
|
||||
|
||||
/** @phpstan-ignore method.notFound */
|
||||
Auth::guard($guard)->login($user);
|
||||
|
||||
/** @phpstan-ignore return.type */
|
||||
return redirect(Workbench::config('start'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the user out of the application.
|
||||
*
|
||||
* @param string|null $guard
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function logout($guard = null)
|
||||
{
|
||||
$guard = $guard ?: config('auth.defaults.guard');
|
||||
|
||||
/** @phpstan-ignore method.notFound */
|
||||
Auth::guard($guard)->logout();
|
||||
|
||||
Session::forget('password_hash_'.$guard);
|
||||
|
||||
/** @phpstan-ignore return.type */
|
||||
return redirect(Workbench::config('start'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Support\Facades\Response;
|
||||
use Orchestra\Workbench\Workbench;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class CatchDefaultRoute
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure(\Illuminate\Http\Request):(mixed) $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
$workbench = Workbench::config();
|
||||
|
||||
if ($request->decodedPath() === '/' && ! \is_null($workbench['user']) && \is_null($request->user())) {
|
||||
return redirect('/_workbench');
|
||||
}
|
||||
|
||||
$response = $next($request);
|
||||
|
||||
if ($request->decodedPath() !== '/') {
|
||||
return $response;
|
||||
}
|
||||
|
||||
if (property_exists($response, 'exception') && ! \is_null($response->exception) && $response->exception instanceof NotFoundHttpException) {
|
||||
if ($workbench['start'] !== '/') {
|
||||
return redirect($workbench['start']);
|
||||
} elseif (
|
||||
($workbench['install'] === true && $workbench['welcome'] !== false)
|
||||
|| ($workbench['install'] === false && $workbench['welcome'] === true)
|
||||
) {
|
||||
return Response::view('welcome');
|
||||
}
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench\Http\Requests\Auth;
|
||||
|
||||
use Illuminate\Auth\Events\Lockout;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class LoginRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'email' => ['required', 'string', 'email'],
|
||||
'password' => ['required', 'string'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to authenticate the request's credentials.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function authenticate(): void
|
||||
{
|
||||
$this->ensureIsNotRateLimited();
|
||||
|
||||
if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
|
||||
RateLimiter::hit($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => trans('auth.failed'),
|
||||
]);
|
||||
}
|
||||
|
||||
RateLimiter::clear($this->throttleKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the login request is not rate limited.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function ensureIsNotRateLimited(): void
|
||||
{
|
||||
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event(new Lockout($this));
|
||||
|
||||
$seconds = RateLimiter::availableIn($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => trans('auth.throttle', [
|
||||
'seconds' => $seconds,
|
||||
'minutes' => ceil($seconds / 60),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the rate limiting throttle key for the request.
|
||||
*/
|
||||
public function throttleKey(): string
|
||||
{
|
||||
return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench\Http\Requests;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Auth\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, 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,20 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench\Listeners;
|
||||
|
||||
use Orchestra\Testbench\Foundation\Events\ServeCommandStarted;
|
||||
use Orchestra\Testbench\Workbench\Actions\AddAssetSymlinkFolders as Action;
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class AddAssetSymlinkFolders
|
||||
{
|
||||
/**
|
||||
* Handle the event.
|
||||
*/
|
||||
public function handle(ServeCommandStarted $event): void
|
||||
{
|
||||
resolve(Action::class)->handle();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench\Listeners;
|
||||
|
||||
use Orchestra\Testbench\Foundation\Events\ServeCommandEnded;
|
||||
use Orchestra\Testbench\Workbench\Actions\RemoveAssetSymlinkFolders as Action;
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class RemoveAssetSymlinkFolders
|
||||
{
|
||||
/**
|
||||
* Handle the event.
|
||||
*/
|
||||
public function handle(ServeCommandEnded $event): void
|
||||
{
|
||||
resolve(Action::class)->handle();
|
||||
}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench;
|
||||
|
||||
use Illuminate\Support\Manager;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class RecipeManager extends Manager implements Contracts\RecipeManager
|
||||
{
|
||||
/**
|
||||
* Create "asset-publish" driver.
|
||||
*/
|
||||
public function createAssetPublishDriver(): Contracts\Recipe
|
||||
{
|
||||
return new Recipes\AssetPublishCommand;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create "create-sqlite-db" driver.
|
||||
*/
|
||||
public function createCreateSqliteDbDriver(): Contracts\Recipe
|
||||
{
|
||||
return $this->commandUsing('workbench:create-sqlite-db', callback: static function () {
|
||||
if (config('database.default') === 'testing') {
|
||||
config(['database.default' => 'sqlite']);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create "drop-sqlite-db" driver.
|
||||
*/
|
||||
public function createDropSqliteDbDriver(): Contracts\Recipe
|
||||
{
|
||||
return $this->commandUsing('workbench:drop-sqlite-db', callback: static function () {
|
||||
if (config('database.default') === 'sqlite') {
|
||||
config(['database.default' => 'testing']);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create "purge-skeleton" driver.
|
||||
*/
|
||||
protected function createPurgeSkeletonDriver(): Contracts\Recipe
|
||||
{
|
||||
return $this->commandUsing('workbench:purge-skeleton');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create "sync-skeleton" driver.
|
||||
*/
|
||||
protected function createSyncSkeletonDriver(): Contracts\Recipe
|
||||
{
|
||||
return $this->commandUsing('workbench:sync-skeleton');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create anonymous command driver.
|
||||
*
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
public function commandUsing(string $command, array $options = [], ?callable $callback = null): Contracts\Recipe
|
||||
{
|
||||
return new Recipes\Command(
|
||||
command: $command,
|
||||
options: $options,
|
||||
callback: $callback,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the recipe by name.
|
||||
*/
|
||||
public function command(string $driver): Contracts\Recipe
|
||||
{
|
||||
return $this->driver($driver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine recipe is available by name.
|
||||
*/
|
||||
public function hasCommand(string $driver): bool
|
||||
{
|
||||
if (isset($this->customCreators[$driver])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$method = 'create'.Str::studly($driver).'Driver';
|
||||
|
||||
return method_exists($this, $method);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default driver name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDefaultDriver()
|
||||
{
|
||||
return 'asset-publish';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench\Recipes;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Orchestra\Workbench\Workbench;
|
||||
|
||||
class AssetPublishCommand extends Command
|
||||
{
|
||||
/**
|
||||
* Contruct a new recipe.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('vendor:publish', []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the command options.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function commandOptions(): array
|
||||
{
|
||||
/** @var array<int, string> $assets */
|
||||
$assets = Workbench::config('assets');
|
||||
|
||||
$tags = (new Collection($assets))
|
||||
->push('laravel-assets')
|
||||
->unique()
|
||||
->all();
|
||||
|
||||
return array_merge($this->options, [
|
||||
'--tag' => $tags,
|
||||
'--force' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench\Recipes;
|
||||
|
||||
use Illuminate\Contracts\Console\Kernel as ConsoleKernel;
|
||||
use Orchestra\Workbench\Contracts\Recipe;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class Command implements Recipe
|
||||
{
|
||||
/**
|
||||
* After completion callback.
|
||||
*
|
||||
* @var (callable(\Symfony\Component\Console\Output\OutputInterface):(void))|null
|
||||
*/
|
||||
public $callback;
|
||||
|
||||
/**
|
||||
* Construct a new recipe.
|
||||
*
|
||||
* @param array<string, mixed> $options
|
||||
* @param (callable(\Symfony\Component\Console\Output\OutputInterface):(void))|null $callback
|
||||
*/
|
||||
public function __construct(
|
||||
public string $command,
|
||||
public array $options = [],
|
||||
?callable $callback = null
|
||||
) {
|
||||
$this->callback = $callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the recipe.
|
||||
*/
|
||||
public function handle(ConsoleKernel $kernel, OutputInterface $output): void
|
||||
{
|
||||
$kernel->call(
|
||||
$this->commandName(), $this->commandOptions(), $output
|
||||
);
|
||||
|
||||
if (\is_callable($this->callback)) {
|
||||
\call_user_func($this->callback, $output);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the command name.
|
||||
*/
|
||||
protected function commandName(): string
|
||||
{
|
||||
return $this->command;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the command options.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function commandOptions(): array
|
||||
{
|
||||
return $this->options;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench;
|
||||
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
use function Orchestra\Sidekick\Filesystem\join_paths;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @phpstan-type TStubFiles array{
|
||||
* config: ?string,
|
||||
* 'config.basic': ?string,
|
||||
* gitignore: ?string,
|
||||
* 'routes.api': ?string,
|
||||
* 'routes.console': ?string,
|
||||
* 'routes.web': ?string,
|
||||
* 'seeders.database': ?string
|
||||
* }
|
||||
*/
|
||||
final class StubRegistrar
|
||||
{
|
||||
/**
|
||||
* Files of stub files overrides.
|
||||
*
|
||||
* @var array<string, ?string>
|
||||
*
|
||||
* @phpstan-var TStubFiles
|
||||
*/
|
||||
protected static array $files = [
|
||||
'config' => null,
|
||||
'config.basic' => null,
|
||||
'gitignore' => null,
|
||||
'routes.api' => null,
|
||||
'routes.console' => null,
|
||||
'routes.web' => null,
|
||||
'seeders.database' => null,
|
||||
];
|
||||
|
||||
/**
|
||||
* Swap stub file by name.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function swap(string $name, ?string $file)
|
||||
{
|
||||
if (\array_key_exists($name, self::$files)) {
|
||||
self::$files[$name] = $file;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the stub file from name.
|
||||
*/
|
||||
public static function file(string $name): ?string
|
||||
{
|
||||
$defaultStub = join_paths(__DIR__, 'Console', 'stubs');
|
||||
|
||||
return transform(
|
||||
Arr::get(array_merge([
|
||||
'config' => join_paths($defaultStub, 'testbench.yaml'),
|
||||
'config.basic' => join_paths($defaultStub, 'testbench.plain.yaml'),
|
||||
'gitignore' => join_paths($defaultStub, 'workbench.gitignore'),
|
||||
'routes.api' => join_paths($defaultStub, 'routes', 'api.php'),
|
||||
'routes.console' => join_paths($defaultStub, 'routes', 'console.php'),
|
||||
'routes.web' => join_paths($defaultStub, 'routes', 'web.php'),
|
||||
'seeders.database' => join_paths($defaultStub, 'database', 'seeders', 'DatabaseSeeder.php'),
|
||||
], array_filter(self::$files)), $name),
|
||||
static function ($file) {
|
||||
$realpath = realpath($file);
|
||||
|
||||
return $realpath !== false ? $realpath : null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace stub namespaces.
|
||||
*/
|
||||
public static function replaceInFile(Filesystem $filesystem, string $filename): void
|
||||
{
|
||||
(new Actions\ReplaceNamespaces($filesystem))->handle($filename);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench\View\Components;
|
||||
|
||||
use Illuminate\View\Component;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class AppLayout extends Component
|
||||
{
|
||||
/**
|
||||
* Get the view / contents that represents the component.
|
||||
*/
|
||||
public function render(): View
|
||||
{
|
||||
return view('layouts.app');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench\View\Components;
|
||||
|
||||
use Illuminate\View\Component;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class GuestLayout extends Component
|
||||
{
|
||||
/**
|
||||
* Get the view / contents that represents the component.
|
||||
*/
|
||||
public function render(): View
|
||||
{
|
||||
return view('layouts.guest');
|
||||
}
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
use function Orchestra\Sidekick\Filesystem\join_paths;
|
||||
use function Orchestra\Testbench\package_path;
|
||||
use function Orchestra\Testbench\workbench;
|
||||
use function Orchestra\Testbench\workbench_path;
|
||||
|
||||
/**
|
||||
* @phpstan-import-type TWorkbenchConfig from \Orchestra\Testbench\Foundation\Config
|
||||
*/
|
||||
class Workbench extends \Orchestra\Testbench\Workbench\Workbench
|
||||
{
|
||||
/**
|
||||
* The Stub Registrar instance.
|
||||
*/
|
||||
protected static ?StubRegistrar $stubRegistrar = null;
|
||||
|
||||
/**
|
||||
* Get the path to the application (Laravel) folder.
|
||||
*
|
||||
* @no-named-arguments
|
||||
*
|
||||
* @param array<int, string|null>|string ...$path
|
||||
*/
|
||||
public static function applicationPath(array|string $path = ''): string
|
||||
{
|
||||
/** @var array<int, string|null> $paths */
|
||||
$paths = Arr::wrap(\func_num_args() > 1 ? \func_get_args() : $path);
|
||||
|
||||
return base_path(join_paths(...$paths));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the Laravel application skeleton.
|
||||
*
|
||||
* @no-named-arguments
|
||||
*
|
||||
* @param array<int, mixed>|string ...$path
|
||||
*
|
||||
* @see \Orchestra\Workbench\Workbench::applicationPath()
|
||||
*/
|
||||
public static function laravelPath(array|string $path = ''): string
|
||||
{
|
||||
return static::applicationPath($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the package folder.
|
||||
*
|
||||
* @no-named-arguments
|
||||
*
|
||||
* @param array<int, string|null>|string ...$path
|
||||
*/
|
||||
public static function packagePath(array|string $path = ''): string
|
||||
{
|
||||
/** @var array<int, string|null> $paths */
|
||||
$paths = Arr::wrap(\func_num_args() > 1 ? \func_get_args() : $path);
|
||||
|
||||
/** @phpstan-ignore argument.type */
|
||||
return package_path(...$paths);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the workbench folder.
|
||||
*
|
||||
* @no-named-arguments
|
||||
*
|
||||
* @param array<int, string|null>|string ...$path
|
||||
*/
|
||||
public static function path(array|string $path = ''): string
|
||||
{
|
||||
/** @var array<int, string|null> $paths */
|
||||
$paths = Arr::wrap(\func_num_args() > 1 ? \func_get_args() : $path);
|
||||
|
||||
/** @phpstan-ignore argument.type */
|
||||
return workbench_path(...$paths);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the availale configuration.
|
||||
*
|
||||
* @return array<string, mixed>|mixed
|
||||
*
|
||||
* @phpstan-return ($key is null ? TWorkbenchConfig : mixed)
|
||||
*/
|
||||
public static function config(?string $key = null): mixed
|
||||
{
|
||||
return ! \is_null($key)
|
||||
? Arr::get(workbench(), $key)
|
||||
: workbench();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve Stub Registrar instance.
|
||||
*/
|
||||
public static function stub(): StubRegistrar
|
||||
{
|
||||
return static::$stubRegistrar ??= new StubRegistrar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Swap stub file by name.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public static function swapFile(string $name, ?string $file): StubRegistrar
|
||||
{
|
||||
return static::stub()->swap($name, $file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the stub file from name.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public static function stubFile(string $name): ?string
|
||||
{
|
||||
return static::stub()->file($name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace Orchestra\Workbench;
|
||||
|
||||
use Composer\InstalledVersions;
|
||||
use Illuminate\Contracts\Events\Dispatcher as EventDispatcher;
|
||||
use Illuminate\Contracts\Foundation\Application;
|
||||
use Illuminate\Contracts\Http\Kernel as HttpKernel;
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
use Illuminate\Foundation\Console\AboutCommand;
|
||||
use Illuminate\Support\Composer;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Orchestra\Canvas\Core\PresetManager;
|
||||
use Orchestra\Testbench\Foundation\Events\ServeCommandEnded;
|
||||
use Orchestra\Testbench\Foundation\Events\ServeCommandStarted;
|
||||
|
||||
use function Orchestra\Sidekick\Filesystem\join_paths;
|
||||
|
||||
class WorkbenchServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->bind('workbench.composer', static fn () => new Composer(new Filesystem));
|
||||
$this->app->singleton(Contracts\RecipeManager::class, static fn (Application $app) => new RecipeManager($app));
|
||||
|
||||
$this->callAfterResolving(PresetManager::class, static function ($manager) {
|
||||
$manager->extend('workbench', fn (Application $app) => new GeneratorPreset($app));
|
||||
|
||||
$manager->setDefaultDriver('workbench');
|
||||
});
|
||||
|
||||
AboutCommand::add('Workbench', static fn () => array_filter([
|
||||
'Version' => InstalledVersions::getPrettyVersion('orchestra/workbench'),
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
$this->booted(function () {
|
||||
$this->loadRoutesFrom((string) realpath(join_paths(__DIR__, '..', 'routes', 'workbench.php')));
|
||||
});
|
||||
|
||||
/** @phpstan-ignore method.notFound */
|
||||
$this->app->make(HttpKernel::class)->pushMiddleware(Http\Middleware\CatchDefaultRoute::class);
|
||||
|
||||
if ($this->app->runningInConsole()) {
|
||||
$this->commands([
|
||||
Console\BuildCommand::class,
|
||||
Console\CreateSqliteDbCommand::class,
|
||||
Console\DropSqliteDbCommand::class,
|
||||
Console\InstallCommand::class,
|
||||
Console\DevToolCommand::class,
|
||||
Console\PurgeSkeletonCommand::class,
|
||||
Console\SyncSkeletonCommand::class,
|
||||
]);
|
||||
|
||||
tap($this->app->make('events'), static function (EventDispatcher $event) {
|
||||
$event->listen(ServeCommandStarted::class, [Listeners\AddAssetSymlinkFolders::class, 'handle']);
|
||||
$event->listen(ServeCommandEnded::class, [Listeners\RemoveAssetSymlinkFolders::class, 'handle']);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user