Introduction
This recipe demonstrates the use of MoonShine
not as an admin panel, but as a personal account under the User
model with login, registration, password recovery, and profile (basic set).
A good example of working with different Layouts
for different pages.
Routes
Let's start with routing, but first, we need to create a few controllers:
-
AuthenticateController
-
ForgotController
-
ProfileController
-
RegisterController
We will declare the routes in routes/web.php
:
<?php use App\Http\Controllers\AuthenticateController;use App\Http\Controllers\ForgotController;use App\Http\Controllers\ProfileController;use App\Http\Controllers\RegisterController;use App\MoonShine\Pages\ResetPasswordPage;use Illuminate\Support\Facades\Route; Route::controller(AuthenticateController::class)->group(function () { Route::get('/login', 'form')->middleware('guest')->name('login'); Route::post('/login', 'authenticate')->middleware('guest')->name('authenticate'); Route::delete('/logout', 'logout')->middleware('auth')->name('logout');}); Route::controller(ForgotController::class)->middleware('guest')->group(function () { Route::get('/forgot', 'form')->name('forgot'); Route::post('/forgot', 'reset'); Route::get('/reset-password/{token}', static fn (ResetPasswordPage $page) => $page)->name('password.reset'); Route::post('/reset-password', 'updatePassword')->name('password.update');}); Route::controller(RegisterController::class)->middleware('guest')->group(function () { Route::get('/register', 'form')->name('register'); Route::post('/register', 'store')->name('register.store');}); Route::controller(ProfileController::class)->middleware('auth')->prefix('profile')->group(function () { Route::get('/', 'index')->name('profile'); Route::post('/', 'update')->name('profile.update');});
We will create ResetPasswordPage
a bit later
Layouts
We will have pages with forms for login, registration, and password recovery, and they will differ in template from the user profile page, so we need to create 2 templates:
-
AppLayout
- for the profile -
FormLayout
- for authentication
AppLayout
Let's start by executing the command to create the template
php artisan moonshine:layout AppLayout --compact
Next, we will assemble the constructor from the components we need
<?php declare(strict_types=1); namespace App\MoonShine\Layouts; use App\MoonShine\Resources\PackageCategoryResource;use App\MoonShine\Resources\PackageResource;use App\MoonShine\Resources\UserResource;use MoonShine\ColorManager\ColorManager;use MoonShine\Contracts\ColorManager\ColorManagerContract;use MoonShine\Laravel\Layouts\CompactLayout;use MoonShine\MenuManager\MenuGroup;use MoonShine\MenuManager\MenuItem;use MoonShine\UI\Components\{Components, Layout\Div, Layout\Body, Layout\Content, Layout\Flash, Layout\Html, Layout\Layout, Layout\Wrapper}; final class AppLayout extends CompactLayout{ protected function getHomeUrl(): string { return route('home'); } public function build(): Layout { return Layout::make([ Html::make([ $this->getHeadComponent(), Body::make([ Wrapper::make([ Div::make([ Flash::make(), Content::make([ Components::make( $this->getPage()->getComponents() ), ]), ])->class('layout-page'), ]), ])->class('theme-minimalistic'), ]) ->customAttributes([ 'lang' => $this->getHeadLang(), ]) ->withAlpineJs() ->withThemes(), ]); }}
FormLayout
php artisan moonshine:layout FormLayout --compact
<?php declare(strict_types=1); namespace App\MoonShine\Layouts; use MoonShine\Laravel\Layouts\CompactLayout;use MoonShine\UI\Components\{Components, FlexibleRender, Heading, Layout\Div, Layout\Body, Layout\Content, Layout\Flash, Layout\Html, Layout\Layout, Layout\Wrapper}; final class FormLayout extends CompactLayout{ protected function getHomeUrl(): string { return route('home'); } public function build(): Layout { return Layout::make([ Html::make([ $this->getHeadComponent(), Body::make([ Div::make([ Div::make([ $this->getLogoComponent(), ])->class('authentication-logo'), Div::make([ Flash::make(), Components::make($this->getPage()->getComponents()), ])->class('authentication-content'), ])->class('authentication'), ]), ]) ->customAttributes([ 'lang' => $this->getHeadLang(), ]) ->withAlpineJs() ->withThemes(), ]); }}
Pages
Let's create MoonShine
pages for displaying data:
LoginPage
Execute the command to create a page, selecting the type Custom
php artisan moonshine:page LoginPage
<?php declare(strict_types=1); namespace App\MoonShine\Pages; use App\MoonShine\Layouts\FormLayout;use MoonShine\Laravel\Pages\Page;use MoonShine\Contracts\UI\ComponentContract;use MoonShine\UI\Components\ActionButton;use MoonShine\UI\Components\FormBuilder;use MoonShine\UI\Components\Layout\Divider;use MoonShine\UI\Components\Layout\Flex;use MoonShine\UI\Components\Layout\LineBreak;use MoonShine\UI\Components\Link;use MoonShine\UI\Fields\Password;use MoonShine\UI\Fields\Switcher;use MoonShine\UI\Fields\Text; class LoginPage extends Page{ protected ?string $layout = FormLayout::class; /** * @return array<string, string> */ public function getBreadcrumbs(): array { return [ '#' => $this->getTitle() ]; } public function getTitle(): string { return $this->title ?: 'LoginPage'; } /** * @return list<ComponentContract> */ protected function components(): iterable { return [ FormBuilder::make() ->class('authentication-form') ->action(route('authenticate')) ->fields([ Text::make('E-mail', 'email') ->required() ->customAttributes([ 'autofocus' => true, 'autocomplete' => 'username', ]), Password::make(__('Password'), 'password') ->required(), Switcher::make(__('Remember me'), 'remember'), ])->submit(__('Log in'), [ 'class' => 'btn-primary btn-lg w-full', ]), Divider::make(), Flex::make([ ActionButton::make(__('Create account'), route('register'))->primary(), Link::make(route('forgot'), __('Forgot password')) ])->justifyAlign('start') ]; }}
Also, note that we specify the required template for the pages
protected ?string $layout = FormLayout::class;
RegisterPage
php artisan moonshine:page RegisterPage
<?php declare(strict_types=1); namespace App\MoonShine\Pages; use App\MoonShine\Layouts\FormLayout;use MoonShine\Contracts\UI\ComponentContract;use MoonShine\Laravel\Pages\Page;use MoonShine\UI\Components\ActionButton;use MoonShine\UI\Components\FormBuilder;use MoonShine\UI\Fields\Password;use MoonShine\UI\Fields\PasswordRepeat;use MoonShine\UI\Fields\Switcher;use MoonShine\UI\Fields\Text; class RegisterPage extends Page{ protected ?string $layout = FormLayout::class; /** * @return array<string, string> */ public function getBreadcrumbs(): array { return [ '#' => $this->getTitle() ]; } public function getTitle(): string { return $this->title ?: 'RegisterPage'; } /** * @return list<ComponentContract> */ protected function components(): iterable { return [ FormBuilder::make() ->class('authentication-form') ->action(route('register.store')) ->fields([ Text::make(__('Name'), 'name')->required(), Text::make('E-mail', 'email') ->required() ->customAttributes([ 'autofocus' => true, 'autocomplete' => 'off', ]), Password::make(__('Password'), 'password') ->required(), PasswordRepeat::make(__('Repeat password'), 'password_confirmation') ->required(), ])->submit(__('Create account'), [ 'class' => 'btn-primary btn-lg w-full', ])->buttons([ ActionButton::make(__('Log in'), route('login')) ]) ]; }}
ForgotPage
php artisan moonshine:page ForgotPage
<?php declare(strict_types=1); namespace App\MoonShine\Pages; use App\MoonShine\Layouts\FormLayout;use MoonShine\Contracts\UI\ComponentContract;use MoonShine\Laravel\Pages\Page;use MoonShine\UI\Components\ActionButton;use MoonShine\UI\Components\FormBuilder;use MoonShine\UI\Components\Layout\Divider;use MoonShine\UI\Components\Layout\Flash;use MoonShine\UI\Components\Layout\Flex;use MoonShine\UI\Components\Link;use MoonShine\UI\Fields\Password;use MoonShine\UI\Fields\Switcher;use MoonShine\UI\Fields\Text; class ForgotPage extends Page{ protected ?string $layout = FormLayout::class; /** * @return array<string, string> */ public function getBreadcrumbs(): array { return [ '#' => $this->getTitle() ]; } public function getTitle(): string { return $this->title ?: 'ForgotPage'; } /** * @return list<ComponentContract> */ protected function components(): iterable { return [ FormBuilder::make() ->class('authentication-form') ->action(route('forgot')) ->fields([ Text::make('E-mail', 'email') ->required() ->customAttributes([ 'autofocus' => true, 'autocomplete' => 'off', ]), ])->submit(__('Reset password'), [ 'class' => 'btn-primary btn-lg w-full', ]), Divider::make(), Flex::make([ ActionButton::make(__('Log in'), route('login'))->primary(), ])->justifyAlign('start') ]; }}
ResetPasswordPage
php artisan moonshine:page ResetPasswordPage
<?php declare(strict_types=1); namespace App\MoonShine\Pages; use App\MoonShine\Layouts\FormLayout;use Illuminate\Contracts\Routing\UrlRoutable;use MoonShine\Contracts\UI\ComponentContract;use MoonShine\Laravel\Pages\Page;use MoonShine\UI\Components\ActionButton;use MoonShine\UI\Components\FormBuilder;use MoonShine\UI\Components\Layout\Divider;use MoonShine\UI\Components\Layout\Flex;use MoonShine\UI\Fields\Hidden;use MoonShine\UI\Fields\Password;use MoonShine\UI\Fields\PasswordRepeat;use MoonShine\UI\Fields\Text; class ResetPasswordPage extends Page{ protected ?string $layout = FormLayout::class; /** * @return array<string, string> */ public function getBreadcrumbs(): array { return [ '#' => $this->getTitle() ]; } public function getTitle(): string { return $this->title ?: 'ForgotPage'; } /** * @return list<ComponentContract> */ protected function components(): iterable { return [ FormBuilder::make() ->class('authentication-form') ->action(route('password.update')) ->fields([ Hidden::make('token')->setValue(request()->route('token')), Text::make('E-mail', 'email') ->setValue(request()->input('email')) ->required() ->readonly(), Password::make(__('Password'), 'password') ->required(), PasswordRepeat::make(__('Repeat password'), 'password_confirmation') ->required(), ])->submit(__('Reset password'), [ 'class' => 'btn-primary btn-lg w-full', ]), ]; }}
ProfilePage
php artisan moonshine:page ProfilePage
<?php declare(strict_types=1); namespace App\MoonShine\Pages; use App\MoonShine\Layouts\AppLayout;use MoonShine\Contracts\UI\ComponentContract;use MoonShine\Laravel\Pages\Page;use MoonShine\UI\Components\FormBuilder;use MoonShine\UI\Components\Layout\Box;use MoonShine\UI\Components\Tabs;use MoonShine\UI\Components\Tabs\Tab;use MoonShine\UI\Fields\Hidden;use MoonShine\UI\Fields\Password;use MoonShine\UI\Fields\PasswordRepeat;use MoonShine\UI\Fields\Text; class ProfilePage extends Page{ protected ?string $layout = AppLayout::class; /** * @return array<string, string> */ public function getBreadcrumbs(): array { return [ '#' => $this->getTitle(), ]; } public function getTitle(): string { return $this->title ?: 'LoginPage'; } /** * @return list<ComponentContract> */ protected function components(): iterable { return [ Box::make([ FormBuilder::make() ->class('authentication-form') ->action(route('profile.update')) ->fill(auth()->user()) ->fields([ Tabs::make([ Tab::make(__('Profile'), [ Text::make(__('Name'), 'name')->required(), Text::make('E-mail', 'email') ->required() ->customAttributes([ 'autofocus' => true, 'autocomplete' => 'off', ]), ]), Tab::make(__('Password'), [ Password::make(__('Password'), 'password'), PasswordRepeat::make(__('Repeat password'), 'password_confirmation'), ])->active( session('errors')?->has('password') ?? false ) ]) ])->submit(__('Update profile'), [ 'class' => 'btn-primary btn-lg w-full', ]), ]), FormBuilder::make() ->name('logout') ->class('authentication-form') ->action(route('logout')) ->fields([ Hidden::make('_method')->setValue('DELETE'), ])->submit(__('Log out'), [ 'class' => 'btn-primary btn-lg w-full', ]), ]; }}
Controllers
AuthenticateController
<?php declare(strict_types=1); namespace App\Http\Controllers; use App\Http\Requests\AuthenticateFormRequest;use App\Models\User;use App\MoonShine\Pages\LoginPage;use Illuminate\Container\Attributes\Auth;use Illuminate\Container\Attributes\Authenticated;use Illuminate\Container\Attributes\CurrentUser;use Illuminate\Contracts\Auth\Guard;use Illuminate\Http\RedirectResponse;use Illuminate\Http\Request; final class AuthenticateController extends Controller{ public function form(LoginPage $page): LoginPage { return $page; } public function authenticate(AuthenticateFormRequest $request): RedirectResponse { if (!auth()->attempt($request->validated())) { return back()->withErrors([ 'email' => __('moonshine::auth.failed') ]); } return redirect()->intended( route('profile') ); } public function logout( #[Auth] Guard $guard, Request $request ): RedirectResponse { $guard->logout(); $request->session()->invalidate(); $request->session()->regenerateToken(); return redirect()->intended( url()->previous() ?? route('home') ); }}
Note how we render pages in controllers:
public function form(LoginPage $page): LoginPage{ return $page;}
I will also provide the FormRequest
classes so that the recipe is as complete as possible.
<?php namespace App\Http\Requests; use Illuminate\Contracts\Validation\ValidationRule;use Illuminate\Foundation\Http\FormRequest;use Illuminate\Validation\Rules\Password; class AuthenticateFormRequest extends FormRequest{ public function authorize(): bool { return auth()->guest(); } /** * Get the validation rules that apply to the request. * * @return array<string, ValidationRule|array|string> */ public function rules(): array { return [ 'email' => ['required'], 'password' => ['required', Password::default()], ]; }}
ForgotController
<?php namespace App\Http\Controllers; use App\Http\Requests\ForgotPasswordFormRequest;use App\Http\Requests\ResetPasswordFormRequest;use App\Models\User;use App\MoonShine\Pages\ForgotPage;use Illuminate\Auth\Events\PasswordReset;use Illuminate\Http\RedirectResponse;use Illuminate\Support\Facades\Hash;use Illuminate\Support\Facades\Password;use Illuminate\Support\Str;use MoonShine\Laravel\MoonShineUI; class ForgotController extends Controller{ public function form(ForgotPage $page): ForgotPage { return $page; } public function reset(ForgotPasswordFormRequest $request): RedirectResponse { $status = Password::sendResetLink( $request->only('email') ); if ($status === Password::RESET_LINK_SENT) { MoonShineUI::toast(__('If the account exists, then the instructions are sent to your email')); } return $status === Password::RESET_LINK_SENT ? back()->with(['alert' => __($status)]) : back()->withErrors(['email' => __($status)]); } public function updatePassword(ResetPasswordFormRequest $request): RedirectResponse { $status = Password::reset( $request->only('email', 'password', 'password_confirmation', 'token'), static function (User $user, string $password) { $user->forceFill([ 'password' => Hash::make($password), ])->setRememberToken(Str::random(60)); $user->save(); event(new PasswordReset($user)); } ); return $status === Password::PASSWORD_RESET ? redirect()->route('login')->with('alert', __($status)) : back()->withErrors(['email' => [__($status)]]); }}
<?php namespace App\Http\Requests; use Illuminate\Contracts\Validation\ValidationRule;use Illuminate\Foundation\Http\FormRequest; class ForgotPasswordFormRequest extends FormRequest{ public function authorize(): bool { return auth()->guest(); } /** * Get the validation rules that apply to the request. * * @return array<string, ValidationRule|array|string> */ public function rules(): array { return [ 'email' => ['required', 'email:dns'], ]; }}
<?php namespace App\Http\Requests; use Illuminate\Contracts\Validation\ValidationRule;use Illuminate\Foundation\Http\FormRequest;use Illuminate\Validation\Rule;use Illuminate\Validation\Rules\Password as PasswordRules; class ResetPasswordFormRequest extends FormRequest{ public function authorize(): bool { return auth()->guest(); } /** * Get the validation rules that apply to the request. * * @return array<string, ValidationRule|array|string> */ public function rules(): array { return [ 'token' => 'required', 'email' => ['required', 'email'], 'password' => ['required', 'confirmed', PasswordRules::default()], ]; }}
ProfileController
<?php declare(strict_types=1); namespace App\Http\Controllers; use App\Http\Requests\ProfileFormRequest;use App\Models\User;use App\MoonShine\Pages\ProfilePage;use Illuminate\Container\Attributes\CurrentUser;use Illuminate\Support\Facades\Hash;use Symfony\Component\HttpFoundation\RedirectResponse; final class ProfileController extends Controller{ public function index( ProfilePage $page ): ProfilePage { return $page; } public function update( ProfileFormRequest $request, #[CurrentUser] User $user ): RedirectResponse { $data = $request->only(['email', 'name']); if ($request->filled('password')) { $data['password'] = Hash::make($request->input('password')); } $user->update($data); return to_route('profile'); }}
<?php namespace App\Http\Requests; use Illuminate\Contracts\Validation\ValidationRule;use Illuminate\Foundation\Http\FormRequest;use Illuminate\Validation\Rule; class ProfileFormRequest extends FormRequest{ public function authorize(): bool { return auth()->check(); } /** * Get the validation rules that apply to the request. * * @return array<string, ValidationRule|array|string> */ public function rules(): array { return [ 'name' => ['required'], 'email' => ['required', 'email:dns', Rule::unique('users')->ignore(auth()->id())], 'password' => ['confirmed'], ]; }}
RegisterController
<?php declare(strict_types=1); namespace App\Http\Controllers; use App\Http\Requests\RegisterFormRequest;use App\Models\User;use App\MoonShine\Pages\RegisterPage;use Illuminate\Http\RedirectResponse; final class RegisterController extends Controller{ public function form(RegisterPage $page): RegisterPage { return $page; } public function store(RegisterFormRequest $request): RedirectResponse { $user = User::query()->create( $request->validated() ); auth()->login($user); return redirect()->intended( route('home') ); }}
<?php namespace App\Http\Requests; use Illuminate\Contracts\Validation\ValidationRule;use Illuminate\Foundation\Http\FormRequest;use Illuminate\Validation\Rule; class RegisterFormRequest extends FormRequest{ public function authorize(): bool { return auth()->guest(); } /** * Get the validation rules that apply to the request. * * @return array<string, ValidationRule|array|string> */ public function rules(): array { return [ 'name' => ['required'], 'email' => ['required', 'email:dns', Rule::unique('users')], 'password' => ['required', 'confirmed'], ]; }}
That's it! MoonShine
does not limit you to just an admin panel, as it is a full-fledged UI kit.