Skip to content

Commit 0d64098

Browse files
committed
initial import
0 parents  commit 0d64098

17 files changed

+899
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/vendor/

README.md

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# Laravel 5.4+ integer sequence helpers
2+
3+
This package provides helpers to work with no-gap and ordered integer sequences in Laravel models.
4+
5+
It has been built to help the generation of "administrative sequences" where there should be no missing value between records (invoices, etc).
6+
7+
**WARNING** This is *beta* version (POC). This is not yet production ready, so you should use at your own risk.
8+
9+
## Installation
10+
11+
Add the service provider to your configuration :
12+
13+
```php
14+
'providers' => [
15+
// ...
16+
17+
Bnb\Laravel\Sequence\SequenceServiceProvider::class,
18+
19+
// ...
20+
],
21+
```
22+
23+
24+
## Configuration
25+
26+
You can customize this package behavior by publishing the configuration file :
27+
28+
php artisan vendor:publish --provider='Bnb\Laravel\Sequence\SequenceServiceProvider'
29+
30+
You can customize values without publishing by specifying those keys in your `.env` file :
31+
32+
```
33+
SEQUENCE_START=123 # defaults to 1
34+
SEQUENCE_QUEUE_CONNECTION=database # defaults to default
35+
SEQUENCE_QUEUE_NAME=sequence # defaults to default
36+
SEQUENCE_AUTO_DISPATCH=false # defaults to true
37+
```
38+
39+
> To avoid concurrency issues when generating sequence number, the queue worker number should be set to one.
40+
It is recommended to use a dedicated queue (and worker).
41+
42+
43+
## Adding a sequence to a model
44+
45+
Sequence columns should be generated with the following configuration in your migration :
46+
47+
$table->unsignedInteger('sequence_name')->nullable()->unique();
48+
49+
To work with sequence you must enhance your model class with the `Bnb\Laravel\Sequence\HasSequence` trait.
50+
51+
The `sequences` array property of your model must contain the list of the sequences names :
52+
53+
```
54+
protected $sequences = ['my_sequence'];
55+
```
56+
57+
Some sequence properties can be overridden by specifying a method in the model class (where `MySequence` is the name your sequence in PascalCase) :
58+
- sequence start value (per-sequence) with the `getMySequenceStartValue() : int`
59+
- sequence format (per-sequence) with the `formatMySequenceSequence($next, $last) : int`
60+
- sequence generation authorization (per-sequence) with the `canGenerateMySequenceSequence() : bool`
61+
- sequence generation queue connection (for the model class) `getSequenceConnection() : string`
62+
- sequence generation queue name (for the model class) `getSequenceQueue() : string`
63+
- sequence auto-dispatch activation (for the model class) `isSequenceAutoDispatch() : bool`
64+
65+
Example :
66+
67+
```
68+
use Bnb\Laravel\Sequence\HasSequence;
69+
use Illuminate\Database\Eloquent\Model;
70+
71+
/**
72+
* MyModel model uses a sequence named
73+
*/
74+
class MyModel extends Model
75+
{
76+
use HasSequence;
77+
78+
const SEQ_INVOICE_NUMBER_START = 0;
79+
80+
public $timestamps = false;
81+
82+
protected $fillable = ['active'];
83+
84+
protected $sequences = ['invoice_number'];
85+
86+
/**
87+
* Assume the sequence can only be generated if active is true
88+
*/
89+
protected function canGenerateReadOnlySequence()
90+
{
91+
return $this->active;
92+
}
93+
94+
/**
95+
* The sequence default value must match the format
96+
*/
97+
protected function getInvoiceNumberStartValue()
98+
{
99+
return sprintf('%1$04d%2$06d', date('Y'), static::SEQ_INVOICE_NUMBER_START);
100+
}
101+
102+
103+
/**
104+
* Format the sequence number with current Year that resets its counter to 0 on year change
105+
*/
106+
protected function formatInvoiceNumberSequence($next, $last)
107+
{
108+
$newYear = date('Y');
109+
$newCounter = substr($next, 4);
110+
111+
if ($last) {
112+
$lastYear = substr($last, 0, 4);
113+
114+
if ($lastYear < $newYear) {
115+
$newCounter = static::SEQ_INVOICE_NUMBER_START;
116+
}
117+
}
118+
119+
return sprintf('%1$04d%2$06d', $newYear, $newCounter);
120+
}
121+
}
122+
```
123+
124+
### Sequence generation event
125+
126+
When a sequence number is generated a model event is thrown for running custom tasks.
127+
You can listen to the sequence event using the `sequenceGenerated($sequenceName, $callback)` method (in a service provider boot method for example) :
128+
129+
```
130+
MyModel::sequenceGenerated('invoice_number', function ($model) {
131+
Mail::to($model->recipient_email)->send(new InvoiceGenerated($model));
132+
});
133+
```
134+
135+
136+
## Schedule generation
137+
138+
You can schedule inside your console kernel the `Bnb\Laravel\Sequence\Console\Commands\UpdateSequence` at the required frequency to generate the missing sequence numbers asynchronously.
139+
140+
```
141+
protected function schedule(Schedule $schedule)
142+
{
143+
$schedule->command('sequence:update')
144+
->hourly();
145+
}
146+
```
147+
148+
This may not be required when using auto dispatch mode but can be used as a security fallback.

composer.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "bnbwebexpertise/laravel-sequence",
3+
"description": "Laravel package to handle model sequence generation (no gap)",
4+
"type": "package",
5+
"license": "MIT",
6+
"authors": [
7+
{
8+
"name": "B&B Web Expertise",
9+
"email": "support@bnb.re"
10+
}
11+
],
12+
"minimum-stability": "stable",
13+
"require": {
14+
"php": ">=5.6.4",
15+
"laravel/framework": ">=5.4.0"
16+
},
17+
"autoload": {
18+
"psr-4": {
19+
"Bnb\\Laravel\\Sequence\\": "src/"
20+
}
21+
},
22+
"autoload-dev": {
23+
"psr-4": {
24+
"Bnb\\Laravel\\Sequence\\Tests\\": "tests/"
25+
}
26+
}
27+
}

config/sequence.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
return [
4+
/*
5+
|--------------------------------------------------------------------------
6+
| Automatic sequence generation
7+
|--------------------------------------------------------------------------
8+
|
9+
| If true will dispatch the sequence generation when the model is saved.
10+
| Otherwise the sequence update job must be run (via cron task or manually).
11+
|
12+
*/
13+
14+
'dispatch' => env('SEQUENCE_AUTO_DISPATCH', true),
15+
16+
/*
17+
|--------------------------------------------------------------------------
18+
| Start values
19+
|--------------------------------------------------------------------------
20+
|
21+
| Configure the sequence prefix and suffix
22+
|
23+
*/
24+
25+
'start' => env('SEQUENCE_START', 1),
26+
27+
/*
28+
|--------------------------------------------------------------------------
29+
| Qeue
30+
|--------------------------------------------------------------------------
31+
|
32+
| The queue to push the jobs onto. Should be run by a single worker to avoid
33+
| concurrency.
34+
|
35+
*/
36+
37+
'queue' => [
38+
'connection' => env('SEQUENCE_QUEUE_CONNECTION', config('queue.default')),
39+
'name' => env('SEQUENCE_QUEUE_NAME', config(sprintf('queue.connections.%s.queue', config('queue.default')))),
40+
],
41+
];

phpunit.xml.dist

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit backupGlobals="false"
3+
backupStaticAttributes="false"
4+
bootstrap="../../../bootstrap/autoload.php"
5+
colors="true"
6+
convertErrorsToExceptions="true"
7+
convertNoticesToExceptions="true"
8+
convertWarningsToExceptions="true"
9+
processIsolation="false"
10+
stopOnFailure="false">
11+
<testsuites>
12+
<testsuite>
13+
<file>tests/SequenceTest.php</file>
14+
</testsuite>
15+
</testsuites>
16+
<filter>
17+
<whitelist processUncoveredFilesFromWhitelist="true">
18+
<directory suffix=".php">./app</directory>
19+
</whitelist>
20+
</filter>
21+
<php>
22+
<env name="APP_ENV" value="testing"/>
23+
<env name="DB_CONNECTION" value="sqlite_testing"/>
24+
<env name="CACHE_DRIVER" value="array"/>
25+
<env name="SESSION_DRIVER" value="array"/>
26+
<env name="QUEUE_DRIVER" value="sync"/>
27+
</php>
28+
</phpunit>
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
namespace Bnb\Laravel\Sequence\Console\Commands;
4+
5+
use Bnb\Laravel\Sequence\Jobs\UpdateSequence as UpdateSequenceJob;
6+
use Illuminate\Console\Command;
7+
use Illuminate\Foundation\Bus\DispatchesJobs;
8+
use Lang;
9+
use Symfony\Component\Console\Input\InputArgument;
10+
11+
class UpdateSequence extends Command
12+
{
13+
14+
use DispatchesJobs;
15+
16+
protected $signature = 'sequence:update';
17+
18+
19+
public function __construct()
20+
{
21+
$this->description = Lang::get('sequence::messages.console.update_description');
22+
23+
parent::__construct();
24+
25+
$this->getDefinition()->addArgument(new InputArgument('class', InputArgument::REQUIRED,
26+
Lang::get('sequence::messages.console.update_argument_class')));
27+
}
28+
29+
30+
public function handle()
31+
{
32+
$class = $this->argument('class');
33+
34+
if ( ! class_exists($class)) {
35+
$this->error(Lang::get('sequence::messages.console.update_error_class_not_found', ['class' => $class]));
36+
37+
return;
38+
}
39+
40+
$connection = config('sequence.queue.connection');
41+
$queue = config('sequence.queue.name');
42+
43+
if (method_exists(self::class, $method = 'getSequenceConnection')) {
44+
$connection = (new self)->{$method}();
45+
}
46+
47+
if (method_exists(self::class, $method = 'getSequenceQueue')) {
48+
$queue = (new self)->{$method}();
49+
}
50+
51+
$this->dispatch((new UpdateSequenceJob($class))->onConnection($connection)->onQueue($queue));
52+
}
53+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
/**
3+
* laravel
4+
*
5+
* @author Jérémy GAULIN <jeremy@bnb.re>
6+
* @copyright 2017 - B&B Web Expertise
7+
*/
8+
9+
namespace Bnb\Laravel\Sequence\Exceptions;
10+
11+
use Bnb\Laravel\Sequence\HasSequence;
12+
use Exception;
13+
14+
class InvalidModelException extends Exception
15+
{
16+
17+
public function __construct($class)
18+
{
19+
parent::__construct(sprintf('Class "%s" must use "%s" trait', $class, HasSequence::class));
20+
}
21+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
/**
3+
* laravel
4+
*
5+
* @author Jérémy GAULIN <jeremy@bnb.re>
6+
* @copyright 2017 - B&B Web Expertise
7+
*/
8+
9+
namespace Bnb\Laravel\Sequence\Exceptions;
10+
11+
use Bnb\Laravel\Sequence\HasSequence;
12+
use Exception;
13+
14+
class InvalidSequenceNumberException extends Exception
15+
{
16+
17+
public function __construct($name, $class, $value)
18+
{
19+
parent::__construct(sprintf('Sequence value "%s" for "%s" in class "%s" must use a 10 digit integer format.', $value, $name, $class,
20+
HasSequence::class));
21+
}
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
/**
3+
* laravel
4+
*
5+
* @author Jérémy GAULIN <jeremy@bnb.re>
6+
* @copyright 2017 - B&B Web Expertise
7+
*/
8+
9+
namespace Bnb\Laravel\Sequence\Exceptions;
10+
11+
use Bnb\Laravel\Sequence\HasSequence;
12+
use Exception;
13+
14+
class SequenceOutOfRangeException extends Exception
15+
{
16+
17+
public function __construct($name, $class, $value)
18+
{
19+
parent::__construct(sprintf('Sequence value "%s" for "%s" in class "%s" is out of range or smaller than last value.', $value, $name,
20+
$class, HasSequence::class));
21+
}
22+
}

0 commit comments

Comments
 (0)