Skip to content

Commit 27211ad

Browse files
committed
Introduce generators, update default alphabet, and remove prefix config and model location.
1 parent 5503807 commit 27211ad

11 files changed

+275
-174
lines changed

config/stripe_ids.php

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,11 @@
44

55
'hash_alphabet' => env(
66
'STRIPE_IDS_HASH_ALPHABET',
7-
'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
7+
'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
88
),
99

1010
'hash_length' => env('STRIPE_IDS_HASH_LENGTH', 16),
1111

12-
// The 'prefixes' key is optional, and is only required if you are using the StripeIds::findByStripeId() method to
13-
// find generic models by their id.
14-
15-
'prefixes' => [
16-
17-
// 'ch_' => \App\Models\Charge::class,
18-
19-
],
12+
'generator' => \Mitchdav\StripeIds\Generators\TimestampFirstGenerator::class,
2013

2114
];

src/Generators/GeneratorInterface.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace Mitchdav\StripeIds\Generators;
4+
5+
interface GeneratorInterface
6+
{
7+
public function generate(int $length): string;
8+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace Mitchdav\StripeIds\Generators;
4+
5+
class RandomBytesGenerator implements GeneratorInterface
6+
{
7+
/**
8+
* @param int $length
9+
* @return string
10+
* @throws \Exception
11+
*/
12+
public function generate(int $length): string
13+
{
14+
return random_bytes($length);
15+
}
16+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
namespace Mitchdav\StripeIds\Generators;
4+
5+
use InvalidArgumentException;
6+
7+
class TimestampFirstGenerator implements GeneratorInterface
8+
{
9+
public const TIMESTAMP_BYTES = 6;
10+
11+
/**
12+
* @param int $length
13+
* @return string
14+
* @throws \Exception
15+
*
16+
* @link https://github.com/ramsey/uuid
17+
*/
18+
public function generate(int $length): string
19+
{
20+
if ($length < self::TIMESTAMP_BYTES || $length < 0) {
21+
throw new InvalidArgumentException(
22+
'Length must be a positive integer greater than or equal to '.self::TIMESTAMP_BYTES
23+
);
24+
}
25+
26+
$time = str_pad(
27+
base_convert($this->timestamp(), 10, 16),
28+
self::TIMESTAMP_BYTES * 2,
29+
'0',
30+
STR_PAD_LEFT
31+
);
32+
33+
$hash = '';
34+
35+
if (self::TIMESTAMP_BYTES > 0 && $length > self::TIMESTAMP_BYTES) {
36+
$hash = random_bytes($length - self::TIMESTAMP_BYTES);
37+
}
38+
39+
return (string) hex2bin(
40+
$time.str_pad(
41+
bin2hex((string) $hash),
42+
$length - self::TIMESTAMP_BYTES,
43+
'0'
44+
)
45+
);
46+
}
47+
48+
/**
49+
* Returns current timestamp a string integer, precise to 0.00001 seconds
50+
*
51+
* @link https://github.com/ramsey/uuid
52+
*/
53+
private function timestamp(): string
54+
{
55+
$time = explode(' ', microtime(false));
56+
57+
return $time[1].substr($time[0], 2, 5);
58+
}
59+
}

src/HasStripeId.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public function getStripeIdHashLength()
4141

4242
public function getStripeIdPrefix()
4343
{
44-
return $this->stripeIdPrefix ?? array_flip(config('stripe_ids.prefixes'))[get_class()] ?? null;
44+
return $this->stripeIdPrefix ?? null;
4545
}
4646

4747
public function getStripeId()

src/ServiceProvider.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace Mitchdav\StripeIds;
44

5+
use Mitchdav\StripeIds\Generators\GeneratorInterface;
6+
57
class ServiceProvider extends \Illuminate\Support\ServiceProvider
68
{
79
public function register()
@@ -17,11 +19,15 @@ public function boot()
1719
__DIR__.'/../config/stripe_ids.php' => config_path('stripe_ids.php'),
1820
], 'config');
1921

22+
$this->app->bind(GeneratorInterface::class, function ($app) {
23+
return $this->app->make($app['config']['stripe_ids']['generator']);
24+
});
25+
2026
$this->app->singleton(StripeIds::class, function ($app) {
2127
return new StripeIds(
28+
$this->app->make(GeneratorInterface::class),
2229
$app['config']['stripe_ids']['hash_length'],
23-
$app['config']['stripe_ids']['hash_alphabet'],
24-
$app['config']['stripe_ids']['prefixes']
30+
$app['config']['stripe_ids']['hash_alphabet']
2531
);
2632
});
2733
}

src/StripeIds.php

Lines changed: 11 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22

33
namespace Mitchdav\StripeIds;
44

5-
use Illuminate\Database\Eloquent\Model;
6-
use Illuminate\Database\Eloquent\ModelNotFoundException;
7-
use Illuminate\Support\Str;
5+
use Mitchdav\StripeIds\Generators\GeneratorInterface;
86

97
class StripeIds
108
{
9+
/**
10+
* @var GeneratorInterface
11+
*/
12+
private $generator;
13+
1114
/**
1215
* @var int
1316
*/
@@ -18,16 +21,11 @@ class StripeIds
1821
*/
1922
private $hashAlphabet;
2023

21-
/**
22-
* @var array
23-
*/
24-
private $prefixes;
25-
26-
public function __construct(int $hashLength, string $hashAlphabet, array $prefixes = [])
24+
public function __construct(GeneratorInterface $generator, int $hashLength, string $hashAlphabet)
2725
{
26+
$this->generator = $generator;
2827
$this->hashLength = $hashLength;
2928
$this->hashAlphabet = $hashAlphabet;
30-
$this->prefixes = $prefixes;
3129
}
3230

3331
public function id(string $prefix, $hashLength = null, $hashAlphabet = null)
@@ -41,30 +39,12 @@ public function hash($length = null, $alphabet = null)
4139
$hashAlphabet = $alphabet ?? $this->hashAlphabet;
4240
$hashAlphabetLength = strlen($hashAlphabet);
4341

44-
return collect(str_split(random_bytes($hashLength)))
42+
$bytes = $this->generator->generate($hashLength);
43+
44+
return collect(str_split($bytes))
4545
->map(function ($randomByte) use ($hashAlphabet, $hashAlphabetLength) {
4646
return $hashAlphabet[ord($randomByte) % $hashAlphabetLength];
4747
})
4848
->join('');
4949
}
50-
51-
public function findByStripeId($id, $prefixes = null)
52-
{
53-
/** @var string $model */
54-
$model = collect($prefixes ?? $this->prefixes)
55-
->first(function ($model, $prefix) use ($id) {
56-
return Str::startsWith($id, $prefix);
57-
});
58-
59-
if ($model !== null) {
60-
/** @var Model $instance */
61-
$instance = app($model);
62-
63-
return $instance
64-
->newModelQuery()
65-
->whereKey($id);
66-
} else {
67-
throw new ModelNotFoundException();
68-
}
69-
}
7050
}

tests/FacadeTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public function it_can_generate_hashes()
1717
/** @test */
1818
public function it_can_generate_ids()
1919
{
20-
$prefix = 'abc';
20+
$prefix = 'abc_';
2121

2222
$id = StripeIds::id($prefix);
2323

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
namespace Mitchdav\StripeIds\Tests\Generators;
4+
5+
use Illuminate\Support\Collection;
6+
use Mitchdav\StripeIds\Generators\RandomBytesGenerator;
7+
use Mitchdav\StripeIds\StripeIds;
8+
use Mitchdav\StripeIds\Tests\TestCase;
9+
10+
class RandomBytesGeneratorTest extends TestCase
11+
{
12+
const HASH_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
13+
14+
const HASH_LENGTH = 16;
15+
16+
const ITERATIONS = 10000;
17+
18+
/** @test */
19+
public function it_can_generate_hashes()
20+
{
21+
$stripeIds = new StripeIds(new RandomBytesGenerator(), self::HASH_LENGTH, self::HASH_ALPHABET);
22+
23+
$hash = $stripeIds->hash();
24+
25+
$this->assertEquals(1, preg_match('/^['.self::HASH_ALPHABET.']+$/', $hash));
26+
$this->assertEquals(self::HASH_LENGTH, strlen($hash));
27+
}
28+
29+
/** @test */
30+
public function it_can_generate_ids()
31+
{
32+
$stripeIds = new StripeIds(new RandomBytesGenerator(), self::HASH_LENGTH, self::HASH_ALPHABET);
33+
34+
$prefix = 'abc_';
35+
36+
$id = $stripeIds->id($prefix);
37+
38+
$this->assertEquals(1, preg_match('/^'.$prefix.'['.self::HASH_ALPHABET.']+$/', $id));
39+
$this->assertEquals(strlen($prefix) + self::HASH_LENGTH, strlen($id));
40+
}
41+
42+
/** @test */
43+
public function it_can_generate_unique_hashes()
44+
{
45+
$stripeIds = new StripeIds(new RandomBytesGenerator(), self::HASH_LENGTH, self::HASH_ALPHABET);
46+
47+
$hashes = Collection::times(self::ITERATIONS)
48+
->map(function () use ($stripeIds) {
49+
return $stripeIds->hash();
50+
});
51+
52+
$this->assertCount(self::ITERATIONS, $hashes->unique());
53+
}
54+
55+
/** @test */
56+
public function it_can_generate_unique_ids()
57+
{
58+
$stripeIds = new StripeIds(new RandomBytesGenerator(), self::HASH_LENGTH, self::HASH_ALPHABET);
59+
60+
$ids = Collection::times(self::ITERATIONS)
61+
->map(function () use ($stripeIds) {
62+
return $stripeIds->id('abc');
63+
});
64+
65+
$this->assertCount(self::ITERATIONS, $ids->unique());
66+
}
67+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php
2+
3+
namespace Mitchdav\StripeIds\Tests\Generators;
4+
5+
use Illuminate\Support\Collection;
6+
use Mitchdav\StripeIds\Generators\TimestampFirstGenerator;
7+
use Mitchdav\StripeIds\StripeIds;
8+
use Mitchdav\StripeIds\Tests\TestCase;
9+
10+
class TimestampFirstGeneratorTest extends TestCase
11+
{
12+
const HASH_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
13+
14+
const HASH_LENGTH = 16;
15+
16+
const ITERATIONS = 10000;
17+
18+
/** @test */
19+
public function it_can_generate_hashes()
20+
{
21+
$stripeIds = new StripeIds(new TimestampFirstGenerator(), self::HASH_LENGTH, self::HASH_ALPHABET);
22+
23+
$hash = $stripeIds->hash();
24+
25+
$this->assertEquals(1, preg_match('/^['.self::HASH_ALPHABET.']+$/', $hash));
26+
$this->assertEquals(self::HASH_LENGTH, strlen($hash));
27+
}
28+
29+
/** @test */
30+
public function it_can_generate_ids()
31+
{
32+
$stripeIds = new StripeIds(new TimestampFirstGenerator(), self::HASH_LENGTH, self::HASH_ALPHABET);
33+
34+
$prefix = 'abc_';
35+
36+
$id = $stripeIds->id($prefix);
37+
38+
$this->assertEquals(1, preg_match('/^'.$prefix.'['.self::HASH_ALPHABET.']+$/', $id));
39+
$this->assertEquals(strlen($prefix) + self::HASH_LENGTH, strlen($id));
40+
}
41+
42+
/** @test */
43+
public function it_can_generate_unique_hashes()
44+
{
45+
$stripeIds = new StripeIds(new TimestampFirstGenerator(), self::HASH_LENGTH, self::HASH_ALPHABET);
46+
47+
$hashes = Collection::times(self::ITERATIONS)
48+
->map(function () use ($stripeIds) {
49+
return $stripeIds->hash();
50+
});
51+
52+
$this->assertCount(self::ITERATIONS, $hashes->unique());
53+
}
54+
55+
/** @test */
56+
public function it_can_generate_unique_ids()
57+
{
58+
$stripeIds = new StripeIds(new TimestampFirstGenerator(), self::HASH_LENGTH, self::HASH_ALPHABET);
59+
60+
$ids = Collection::times(self::ITERATIONS)
61+
->map(function () use ($stripeIds) {
62+
return $stripeIds->id('abc_');
63+
});
64+
65+
$this->assertCount(self::ITERATIONS, $ids->unique());
66+
}
67+
68+
/** @test */
69+
public function it_can_generate_mostly_sequential_hashes()
70+
{
71+
$stripeIds = new StripeIds(new TimestampFirstGenerator(), self::HASH_LENGTH, self::HASH_ALPHABET);
72+
73+
$ids = Collection::times(self::ITERATIONS)
74+
->map(function () use ($stripeIds) {
75+
return $stripeIds->hash();
76+
});
77+
78+
$afterPrevious = $ids->map(function ($id, $index) use ($ids) {
79+
if ($index === 0) {
80+
return true;
81+
}
82+
83+
// We subtract 2 from the TIMESTAMP_BYTES length because our StripeIds::hash method takes the modulo of
84+
// each byte in the set of generated bytes against the size of the alphabet, which can cause the last bytes
85+
// of the timestamp to not be sorted sequentially.
86+
return strncmp($ids[$index - 1], $id, TimestampFirstGenerator::TIMESTAMP_BYTES - 2) <= 0;
87+
});
88+
89+
$allAreAfterPrevious = $afterPrevious->every(function ($isAfterPrevious) {
90+
return $isAfterPrevious === true;
91+
});
92+
93+
$this->assertTrue($allAreAfterPrevious);
94+
}
95+
}

0 commit comments

Comments
 (0)