In my last post in How I believe Xero pulled off implementing their Invoice Numbering System, I showed you how I created a customisable and incremental invoice numbering system using Ruby on Rails. This post will demonstrate how it's done using PHP (Laravel 6).
To recap, we have two sections that make up an invoice number:
- The Prefix
- A numeric value
This approach is customisable as a user can change the prefix to match their business needs and start the sequential value at any number ie 001, 0001 or 1,
max ten characters. 001 is widely used.
In your terminal, run:
php artisan make:migration create_invoice_settings_table
Now, edit the migration file to match below:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateInvoiceSettingsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('invoice_settings', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('organisation_id')->unsigned();
$table->string('prefix')->default('INV-');
$table->string('number_sequence')->default('001');
// You may have other fields...
// The association
$table->foreign('organisation_id')->references('id')->on('organisations');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('invoice_settings');
}
}
Run your migration to create the table with default values.
A new InvoiceSettings should be created when a organisation is added to your system, that way we have the default data assigned to an organisation or create a Seeder to create this recored for the current organisation.
Our InvoiceSettings controller would look as follows:
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
// use App\InvoiceSettings;
class InvoiceSettingsController extends Controller
{
// A constructor will not be used in this example.
/**
* Gets the defaults values.
*
* @param int $organisationId
* @return Response
*/
public function show($organisationId)
{
//
}
/**
* Update the invoice settings for the given organisation.
*
* @param request $request
* @return Response
*/
public function update(Request $request)
{
//
}
}
Go ahead and create an invoice settings model with migration etc then uncomment InvoiceSettings alias in the above when finished.
Validation
Most Laravel developer place their validations in the controller, we won't be doing that, you should not be doing that. Our controllers should be easily testable, slim and saves item to the database/retrieves items from database etc.
If you have created a route for localhost:4000/organisations/1/invoice-settings
, you should see a form with two fields, populating the default invoice settings values. Go ahead and implement that yourself by using an api route or using the blade template.
Once you've done that, we'll create a Laravel Form Request
# Spell this how you see fit
php artisan make:request ShouldBeSequentializeAndUnique
[...]
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
[...]
/**
* Get the custom validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return $this->customRule();
}
/**
* Validate sequence number is numeric and not already in database.
*
* @return array|exception
*/
private function customRule()
{
$prefix = $this->prefix;
$numberSequence = $this->number_sequence;
// In most cases, you should have this already in your controller,
// but we need it here also.
$organisationId = request()->organisation_id;
$invoiceReferenceNumber = strtolower($prefix . $numberSequence); // we now/should have something like inv-001
$rules = [
'prefix' => 'required|string|max:10',
'organisation_id' => 'required|integer',
'number_sequence' => [
'required', 'numeric', 'digits_between:1,10',
Rule::unique('some-table')->where(function ($query) use ($organisationId, $invoiceReferenceNumber) {
$q = $query
->whereRaw('LOWER(number) LIKE ?', '%' . $invoiceReferenceNumber . '%')
->where('organisation_id', $organisationId)->first();
if ($q) {
throw ValidationException::withMessages(['number_sequence' => 'Sequence number already in use']);
}
return $q;
})
],
];
return $rules;
}
WOW! What's going here? For starters, prefix
should be a string with a max length of 10 characters. number_sequence
should be numeric with a max length of 10 characters. Noticed we have not used max:10
as the input field type should be number
. When it's set to type=number
, max:10
sums up the value; instead, we use digits_between
1 and 10.
What is some-table
? For your homework, create another migration called invoices
with a column called number
of type string (required), then replace some-table
with invoices
. Obviously you'll need other columns etc.
Our Rule
searches for invoices with the same $invoiceReferenceNumber
and if found then $numberSequence
is already in use as we combined prefix + numberSequence to make an invoice reference number.
Generating an invoice with number iNv-001
will throw an error*. Generating an invoice with ii-001
will not throw an error* as the user only wants to change the prefix
.
This error* only refers to updating the invoice settings, not when creating an invoice. For your homework, create a validation to ensure number
is unique for all invoices but only per user's organisation.
Our controller should now look like:
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Http\Requests\ShouldBeSequentializeAndUnique;
use App\InvoiceSettings;
class InvoiceSettingsController extends Controller
{
// A constructor will not be used in this example.
/**
* Update the invoice settings for the given organisation.
*
* @param ShouldBeSequentializeAndUnique $request
* @return Json
*/
public function update(ShouldBeSequentializeAndUnique $request)
{
// Run the validation.
$validated = $request->validated();
// $organisationId comes from session or the param.
// Everything should be ok from here.
InvoiceSettings::where('organisation_id', $organisationId)
->update([
'prefix' => $request->prefix,
'number_sequence' => $request->number_sequence
]);
return response()->json($validated, 200);
}
}
EDIT per comment
public function update(ShouldBeSequentializeAndUnique $request)
{
// $organisationId comes from session or the param.
// Everything should be ok from here.
$settings = InvoiceSettings::updateOrCreate(
['organisation_id' => $request->organisation_id],
[
'prefix' => $request->prefix,
'number_sequence' => $request->number_sequence
]);
return response()->json($settings, 200);
}
There are many ways write code. The above maybe incorrect for some but, as we all say, "it works for me" ๐. I'm open for improvements as I haven't been professionally using Laravel for years.
In another post, I'll demonstrate how to increment the number_sequence
on every invoice creation.
Top comments (3)
FormRequest is a child class of Request, so doing this:
feels like an anti-pattern, you rely on the container to give you the current request, while in a child class of request. you can simply do:
or even use any method available in request class:
Awesome! Thanks for that!
Thank you! I shall use and update.