Phần cuối về Laravel 8 Package - How To Create A Highly Configurable

29th Aug 2021
Table of contents

Introduction

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

Bạn thấy bài viết này như thế nào?
0 reactions

Add new comment

Image CAPTCHA
Enter the characters shown in the image.
Câu nói tâm đắc: “Điều tuyệt với nhất trong cuộc sống là làm được những việc mà người khác tin là không thể!”

Related Articles

Mỗi kết nối cơ sở dữ liệu được định nghĩa trong một mảng, với tên kết nối là khóa của mảng

Eager Loading là một kỹ thuật tối ưu hóa truy vấn cơ sở dữ liệu trong Laravel, giúp tăng tốc độ truy vấn và giảm số lượng truy vấn cần thiết để lấy dữ liệu liên quan đến một bản ghi.

Để sử dụng Eager Loading với điều kiện trong Laravel, bạn có thể sử dụng phương thức whereHas hoặc orWhereHas trong Eloquent Builder.

E hiểu đơn giản vầy nha. auth() hay Auth trong laravel là những function global hay class, nó cũng chỉ là 1 thôi

Xin chào các bạn, tuần này mình sẽ viết một bài về cách xử lý Real Time(thời gian thực) với Laravel và Pusher