Phần cuối về Laravel 8 Package - How To Create A Highly Configurable
29th Aug 2021Introduction
For this tutorial we will create a custom authentication package that will do the following:
- Allow users to register, login and logout of the Laravel web app
- Allow users to configure custom views, validation, and methods for these functions.
Set Up The Environment
To create our package we need a way to load the package locally. To do this we will create a fresh Laravel site and configure the package to work through an autoloader.
in you terminal, create a new Laravel site how you normally would. You can also skip this step if you are using an existing site.
laravel new my-cool-site
Next you want to create a directory that will house your package code and navigate into that directory.
mkdir packages && cd packages mkdir CustomAuth && cd CustomAuth
Once inside the directory we need to initialize a composer file that we will use to autoload our package.
composer init
follow the on screen prompts to create a composer.json file.
The file will look something like this:
{ "name": "devingray/custom-auth", "require": {} }
Now we need to get this file "Laravel Ready"
{ "name": "devingray/custom-auth", "description": "Custom Auth", "license": "MIT", "require": { "php": "^7.4|^8.0", "illuminate/contracts": "^8.0" }, "require-dev": { "orchestra/testbench": "^6.0", "phpunit/phpunit": "^9.3" }, "autoload": { "psr-4": { "DevinGray\\CustomAuth\\": "src" } }, "autoload-dev": { "psr-4": { "DevinGray\\CustomAuth\\Tests\\": "tests" } }, "config": { "sort-packages": true }, "extra": { "laravel": { "providers": [ "DevinGray\\CustomAuth\\Providers\\CustomAuthServiceProvider" ] } } }
The important part to note here is the "autoload": section. We are telling composer that all files within the src directory will be part of our package.
Additionally with the following
"extra": { "laravel": { "providers": [ "DevinGray\\CustomAuth\\Providers\\CustomAuthServiceProvider" ] } }
We are providing a way for Laravel to discover our package.
Folder Structure.
Now that we have the composer file ready we need to create our folder structure. It should look something like this
resources --- views routes src --- Classes --- Http --- Controllers --- Requests --- Responses --- Providers composer.json
- resources will hold the views
- routes will tell Laravel how to load the views
- src will contain all of our logic.
Next in the Providers directory, let's create a service provider and initialize that for our first load into Laravel.
To do that, we need to create a class called CustomAuthServiceProvider.php and extend Illuminate\Support\ServiceProvider
<?php namespace DevinGray\CustomAuth\Providers; use Illuminate\Support\ServiceProvider; class CustomAuthServiceProvider extends ServiceProvider { public function boot() { } }
This is enough to get us started.
Loading the package into Laravel.
To load the package we need to require it via composer. Because the package is not yet uploaded to Packagist, we need to tell our main application where to find it.
To do that. Open the main composer.json file for the Laravel project and add the following to it
"repositories": [ { "type": "path", "url": "./packages/CustomAuth" } ],
This basically tells composer to also look inside this folder for packages.
Now you are able to require the package into your application
composer require devingray/custom-auth
If all is done correctly, you will see the following in the console
Using version dev-master for devingray/custom-auth ./composer.json has been updated Running composer update devingray/custom-auth Loading composer repositories with package information Updating dependencies Lock file operations: 1 install, 0 updates, 0 removals - Locking devingray/custom-auth (dev-master) Writing lock file Installing dependencies from lock file (including require-dev) Nothing to install, update or remove Generating optimized autoload files > Illuminate\Foundation\ComposerScripts::postAutoloadDump > @php artisan package:discover --ansi Discovered Package: devingray/custom-auth Discovered Package: facade/ignition Discovered Package: fideloper/proxy Discovered Package: fruitcake/laravel-cors Discovered Package: laravel/sail Discovered Package: laravel/tinker Discovered Package: nesbot/carbon Discovered Package: nunomaduro/collision Package manifest generated successfully. 73 packages you are using are looking for funding. Use the `composer fund` command to find out more!
Great! Now We have a package available to use within the main application.
Creating the Routes and Views
So now that we have our application loading our custom package, let's create register route to allow the users to see a register form.
to do that we need to create a file in our packages resources/views directory.
register.blade.php
NOTE: Normally you would add a layout, but for the sake of this tutorial, we will just add a form to the pages.
<form action="{{ route('register.attempt') }}" method="post"> @csrf <input type="text" name="name" value="{{ old('name') }}" placeholder="Name"> <input type="email" name="email" value="{{ old('email') }}" placeholder="Email"> <input type="password" name="password" value="{{ old('password') }}" placeholder="Password"> <input type="password" name="password_confirmation" value="{{ old('password_confirmation') }}" placeholder="Password Confirm"> <button type="submit"> Register </button> </form>
Great! Now we need to create a route for this.
in our routes directory, add a new file called custom-auth.php and add the following
<?php use Illuminate\Support\Facades\Route; Route::middleware('web')->group(function () { Route::middleware('guest')->group(function () { Route::get('register', function () { dd('Register Route is Loading'); }); }); });
To load it we need to add the following to the boot method of our CustomAuthServiceProvider.php
<?php namespace DevinGray\CustomAuth\Providers; use Illuminate\Support\ServiceProvider; class CustomAuthServiceProvider extends ServiceProvider { public function boot() { $this->loadRoutesFrom(__DIR__.'/../../routes/custom-auth.php'); } }
Now if we navigate to /register in the browser we should see the Register Route is Loading and we know our routes are working.
Great!
Let's configure the route to load our view as well as dump a response when the submit button is clicked.
in CustomAuthServiceProvider.php add the following
<?php namespace DevinGray\CustomAuth\Providers; use Illuminate\Support\ServiceProvider; class CustomAuthServiceProvider extends ServiceProvider { public function boot() { $this->loadRoutesFrom(__DIR__.'/../../routes/custom-auth.php'); $this->loadViewsFrom(__DIR__.'/../../resources/views', 'custom-auth'); } }
and change the routes file to look like this.
<?php use Illuminate\Support\Facades\Route; Route::middleware('web')->group(function () { Route::middleware('guest')->group(function () { Route::get('register', function () { return view('custom-auth::register'); })->name('register'); Route::post('register', function (\Illuminate\Http\Request $request) { dd($request); })->name('register.attempt'); }); });
If all is done correctly you will see your form when you visit /register in the browser and if you click the submit button. You will see the request dumped onto the screen.
in the src/Http/Controllers directory, add a new class called RegisterController.php
<?php namespace DevinGray\CustomAuth\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Routing\Controller; class RegisterController extends Controller { public function create() { return view('custom-auth::register'); } public function store(Request $request) { dd($request); } }
and alter the routes file to look like this
<?php use DevinGray\CustomAuth\Http\Controllers\RegisterController; use Illuminate\Support\Facades\Route; Route::middleware('web')->group(function () { Route::middleware('guest')->group(function () { Route::get('register', [RegisterController::class, 'create'])->name('register'); Route::post('register', [RegisterController::class, 'store'])->name('register.attempt'); }); });
This will have the same effect as before and you should be able to see the same result.
Now we want to create a user through this form and redirect them to a hidden page that requires authentication. To do that we can add a new route and view like so.
<?php use DevinGray\CustomAuth\Http\Controllers\RegisterController; use Illuminate\Support\Facades\Route; Route::middleware('web')->group(function () { Route::middleware('guest')->group(function () { Route::get('register', [RegisterController::class, 'create'])->name('register'); Route::post('register', [RegisterController::class, 'store'])->name('register.attempt'); }); Route::middleware('auth')->group(function () { Route::get('home', function () { return view('custom-auth::home'); })->name('home'); }); });
And create a new view file
home.blade.php
<div> If you see this... You are logged in! </div>
This page should only be accessed if the user is logged in. So let's build the functionality to register a user and then redirect them to the home view.
** Remember We Will Build This For Maximum Configuration**
To Start, add a new Class inside src/Classes called CustomRegister.php
<?php namespace DevinGray\CustomAuth\Classes; class CustomRegister { // }
We can leave it empty for now.
What we want to achieve with this class is the following:
- Add our validation rules
- Add a function to show the register view
- Add a function to create a new user and redirect to /home
We could do all of this directly in the controller, I know, but you will see why later in the post.
Validation
To start this off, let's begin with the validation logic.
inside src/Http/Requests let's create a new request called RegisterRequest.php
<?php namespace DevinGray\CustomAuth\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class RegisterRequest extends FormRequest { public function rules(): array { return []; } }
Now on our CustomRegister.php class, let's build in those rules.
create a new function that looks like this.
public static function validationRules(): array { return [ 'name' => 'required', 'email' => 'required', 'password' => 'required' ]; }
We will then make use of this function inside the Request
<?php namespace DevinGray\CustomAuth\Http\Requests; use DevinGray\CustomAuth\Classes\CustomRegister; use Illuminate\Foundation\Http\FormRequest; class RegisterRequest extends FormRequest { public function rules(): array { return CustomRegister::validationRules(); } }
Now we need to link that Request to the controller
<?php namespace DevinGray\CustomAuth\Http\Controllers; use DevinGray\CustomAuth\Http\Requests\RegisterRequest; use Illuminate\Routing\Controller; class RegisterController extends Controller { public function create() { return view('custom-auth::register'); } public function store(RegisterRequest $request) { dd($request); } }
And now if we submit our form without any inputs, it will return back without doing anything.
So let's add some errors to our blade file.
<form action="{{ route('register.attempt') }}" method="post"> @csrf <input type="text" name="name" value="{{ old('name') }}" placeholder="Name"> @error('name') {{ $message }} @enderror <input type="email" name="email" value="{{ old('email') }}" placeholder="Email"> @error('email') {{ $message }} @enderror <input type="password" name="password" value="{{ old('password') }}" placeholder="Password"> @error('password') {{ $message }} @enderror <input type="password" name="password_confirmation" value="{{ old('password_confirmation') }}" placeholder="Password Confirm"> <button type="submit"> Register </button> </form>
Our RegisterController still has a reference to
return view('custom-auth::register');
Let's extract that to the CustomRegister Class
To do that we need to create the function on the CustomRegister class
Add the following to the RegisterController
<?php namespace DevinGray\CustomAuth\Http\Controllers; use DevinGray\CustomAuth\Classes\CustomRegister; use DevinGray\CustomAuth\Http\Requests\RegisterRequest; use Illuminate\Routing\Controller; class RegisterController extends Controller { protected $customRegister; public function __construct(CustomRegister $customRegister) { $this->customRegister = $customRegister; } public function create() { return $this->customRegister->showRegisterView(); } public function store(RegisterRequest $request) { dd($request); } }
And we now create the function on the CustomRegister class
<?php namespace DevinGray\CustomAuth\Classes; class CustomRegister { public static function validationRules(): array { return [ 'name' => 'required', 'email' => 'required', 'password' => 'required' ]; } public function showRegisterView() { return view('custom-auth::register'); } }
Now the View is being loaded through our CustomRegister class.
Creating the user
To create the user, we need to have a function loaded into the class that does this and redirects the user to the /home route
Change the Register Controller to look like this.
<?php namespace DevinGray\CustomAuth\Http\Controllers; use DevinGray\CustomAuth\Classes\CustomRegister; use DevinGray\CustomAuth\Http\Requests\RegisterRequest; use Illuminate\Routing\Controller; class RegisterController extends Controller { protected $customRegister; public function __construct(CustomRegister $customRegister) { $this->customRegister = $customRegister; } public function create() { return $this->customRegister->showRegisterView(); } public function store(RegisterRequest $request) { return $this->customRegister->createNewUser($request); } }
and let's build that function up on the CustomRegister class.
<?php namespace DevinGray\CustomAuth\Classes; use App\Models\User; use Illuminate\Support\Facades\Auth; class CustomRegister { public static function validationRules(): array { return [ 'name' => 'required', 'email' => 'required', 'password' => 'required' ]; } public function showRegisterView() { return view('custom-auth::register'); } public function createNewUser($request) { $user = User::create([ 'name' => $request->input('name'), 'email' => $request->input('email'), 'password' => bcrypt($request->input('password')), ]); Auth::login($user); return redirect()->route('home'); } }
Now when you add a name, email and password, you will create a new user and login to the application.
For the next steps we will do the following:
- Add a function that allows the user to define custom validation rules.
- Add a function that allows the user to define a custom register route.
Custom Validation Rules.
In Laravel, ServiceProviders are the in my opinion the best place for this logic. (Although most packages will provide config files).
To do this customization, let's open app\Providers\AppServiceProvider.php
and add the following to the boot method
app/Providers/AppServiceProvider.php
<?php namespace App\Providers; use DevinGray\CustomAuth\Classes\CustomRegister; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { public function boot() { CustomRegister::customValidationRules([ 'name' => 'required|min:6', 'email' => 'required|email|unique:users', 'password' => 'required|confirmed|min:8' ]); } }
Let's make it work. By adding a function to assign those rules to a static property, and then checking if that property exists and if so, use those, and if not use the defaults.
<?php namespace DevinGray\CustomAuth\Classes; use App\Models\User; use Illuminate\Support\Facades\Auth; class CustomRegister { public static $customValidationRules; public static function customValidationRules(array $rules) { static::$customValidationRules = $rules; } public static function validationRules(): array { return static::$customValidationRules ? static::$customValidationRules : [ 'name' => 'required', 'email' => 'required', 'password' => 'required' ]; } public function showRegisterView() { return view('custom-auth::register'); } public function createNewUser($request) { $user = User::create([ 'name' => $request->input('name'), 'email' => $request->input('email'), 'password' => bcrypt($request->input('password')), ]); Auth::login($user); return redirect()->route('home'); } }
Now when we submit the form, it is using these rules that are defined in our AppServiceProvider.php
For this, we will allow the user to register a function that can be used in place of return view('custom-auth::register')
So again, let's add this to the AppServiceProvider.php
<?php namespace App\Providers; use DevinGray\CustomAuth\Classes\CustomRegister; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { public function boot() { CustomRegister::customValidationRules([ 'name' => 'required|min:6', 'email' => 'required|email|unique:users', 'password' => 'required|confirmed|min:8' ]); CustomRegister::customRegisterView(function () { return view('custom-view'); }); } }
And let's wire it up to work.
<?php namespace DevinGray\CustomAuth\Classes; use App\Models\User; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; class CustomRegister { public static $customValidationRules; public static $customRegisterView; public static function customValidationRules(array $rules) { static::$customValidationRules = $rules; } public static function validationRules(): array { return static::$customValidationRules ? static::$customValidationRules : [ 'name' => 'required', 'email' => 'required', 'password' => 'required' ]; } public static function customRegisterView(callable $callback) { static::$customRegisterView = $callback; } public function showRegisterView() { return static::$customRegisterView ? call_user_func(static::$customRegisterView) : view('custom-auth::register'); } public function createNewUser($request) { $user = User::create([ 'name' => $request->input('name'), 'email' => $request->input('email'), 'password' => bcrypt($request->input('password')), ]); Auth::login($user); return redirect()->route('home'); } }
Now when you navigate to '/register' in the browser, the custom code will run.
Conclusion
This is a great way to make Laravel packages highly configurable.
This method is used in a number of official Laravel packages such as Laravel/Fortify and Laravel/Spark
Add new comment