Repository: Propaganistas/Laravel-FakeId
Branch: master
Commit: 908618820044
Files: 18
Total size: 23.0 KB
Directory structure:
gitextract__7jf7qmp/
├── .gitattributes
├── .github/
│ ├── FUNDING.yml
│ └── workflows/
│ └── test.yml
├── .gitignore
├── LICENSE
├── README.md
├── composer.json
├── config/
│ └── config.php
├── phpunit.xml.dist
├── src/
│ ├── Commands/
│ │ └── FakeIdSetupCommand.php
│ ├── Facades/
│ │ └── FakeId.php
│ ├── FakeIdServiceProvider.php
│ └── RoutesWithFakeIds.php
└── tests/
├── Entities/
│ ├── Deletable.php
│ ├── Fake.php
│ ├── FakeWithRouteKeyName.php
│ └── Real.php
└── FakeIdTest.php
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
/.gitattributes export-ignore
/.github export-ignore
/.gitignore export-ignore
/tests export-ignore
/phpunit.xml.dist export-ignore
================================================
FILE: .github/FUNDING.yml
================================================
github: Propaganistas
================================================
FILE: .github/workflows/test.yml
================================================
name: Tests
on:
push:
pull_request:
schedule:
- cron: '0 0 * * 1'
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: [ 8.0, 8.1, 8.2, 8.3 ]
laravel: [ 9, 10 ]
stability: [ prefer-lowest, prefer-stable ]
exclude:
- php: 8.0
laravel: 10
name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} (${{ matrix.stability }})
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: dom, curl, json, libxml, mbstring, zip
tools: composer:v2
coverage: none
# https://github.com/briannesbitt/Carbon/releases/tag/2.62.1
- name: Patch Carbon version
if: matrix.php == 8.2 || matrix.php == 8.3
run: |
composer require "nesbot/carbon=^2.63" --dev --no-interaction --no-update
- name: Install dependencies
run: |
composer require "illuminate/support=^${{ matrix.laravel }}" --no-interaction --no-update
composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress
- name: Execute tests
run: vendor/bin/phpunit --verbose
================================================
FILE: .gitignore
================================================
/vendor
/.idea
composer.phar
composer.lock
.phpunit.result.cache
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2015
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# 🚨 ABANDONED
Looking for a new maintainer.
If no maintainer is found by December 2025, this repository will be deleted.
Use [hashids](https://github.com/vinkla/hashids) instead.
---
---
---
# Laravel FakeID

[](https://packagist.org/packages/propaganistas/laravel-fakeid)
[](https://packagist.org/packages/propaganistas/laravel-fakeid)
[](https://packagist.org/packages/propaganistas/laravel-fakeid)
Enables automatic Eloquent model ID obfuscation in routes using [Optimus](https://github.com/jenssegers/optimus).
### Installation
1. Run the Composer require command to install the package
```bash
composer require propaganistas/laravel-fakeid
```
2. The package will automatically register itself.
3. Run the following artisan command to auto-initialize the package's settings
```bash
php artisan fakeid:setup
```
### Usage
Simply import the `RoutesWithFakeIds` trait into your model:
```php
use Illuminate\Database\Eloquent\Model;
use Propaganistas\LaravelFakeId\RoutesWithFakeIds;
class MyModel extends Model
{
use RoutesWithFakeIds;
}
```
All routes generated for this particular model will expose a **fake** ID instead of the raw primary key. Moreover incoming requests containing those fake IDs are automatically converted back to a real ID. The obfuscation layer is therefore transparent and doesn't require you to rethink everything. Just use Laravel as you normally would.
### Example ###
Assuming an `Article` model having a named `show` route.
`routes/web.php`:
```php
Route::get('articles/{article}', 'ArticleController@show')->name('articles.show');
```
`app/Article.php`
```php
use Illuminate\Database\Eloquent\Model;
use Propaganistas\LaravelFakeId\RoutesWithFakeIds;
class Article extends Model
{
use RoutesWithFakeIds;
}
```
A route to this specific endpoint can now be generated using Laravel's `route()` helper, and it will automatically contain a **fake** ID:
```php
{{ $article->name }}
```
================================================
FILE: composer.json
================================================
{
"name": "propaganistas/laravel-fakeid",
"description": "Automatic ID obfuscation for Eloquent models.",
"keywords": [
"laravel",
"optimus",
"hashids",
"fakeid",
"obfuscation"
],
"license": "MIT",
"authors": [
{
"name": "Propaganistas",
"email": "Propaganistas@users.noreply.github.com"
}
],
"require": {
"php": "^8.0",
"illuminate/config": "^9.0|^10.0",
"illuminate/container": "^9.0|^10.0",
"illuminate/routing": "^9.0|^10.0",
"illuminate/support": "^9.0|^10.0",
"jenssegers/optimus": "^1.0"
},
"require-dev": {
"orchestra/testbench": "*",
"phpunit/phpunit": "^9.5.10"
},
"autoload": {
"psr-4": {
"Propaganistas\\LaravelFakeId\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Propaganistas\\LaravelFakeId\\Tests\\": "tests/"
}
},
"extra": {
"laravel": {
"providers": [
"Propaganistas\\LaravelFakeId\\FakeIdServiceProvider"
],
"aliases": {
"FakeId": "Propaganistas\\LaravelFakeId\\Facades\\FakeId"
}
}
},
"minimum-stability": "dev",
"prefer-stable": true,
"suggest": {
"vinkla/hashids": "Laravel-FakeId is deprecated"
}
}
================================================
FILE: config/config.php
================================================
env('FAKEID_PRIME', 961748927),
'inverse' => env('FAKEID_INVERSE', 1430310975),
'random' => env('FAKEID_RANDOM', 620464665),
];
================================================
FILE: phpunit.xml.dist
================================================
./src
./src/Commands/FakeIdSetupCommand.php
./tests/
================================================
FILE: src/Commands/FakeIdSetupCommand.php
================================================
hasExistingConfiguration($env)) {
if ($this->option('preserve')) {
return;
}
if (! $this->option('overwrite')) {
if (! $this->confirm("Overwrite existing configuration?")) {
return;
}
}
$this->removeExistingConfiguration($env);
}
$this->writeNewConfiguration($env, $path);
$this->info("FakeId configured correctly.");
}
/**
* Checks if the given file array contains existing FakeId configuration.
*/
protected function hasExistingConfiguration($file)
{
return Str::contains(implode(' ', $file), 'FAKEID_');
}
/**
* Removes existing FakeId configuration from the given file array.
*
* @param array $file
*/
protected function removeExistingConfiguration(&$file)
{
foreach ($file as $k => $line) {
if (strpos($line, 'FAKEID_') === 0) {
unset($file[$k]);
}
}
}
/**
* Writes new configuration using the provided file array to the given path.
*
* @param array $file
* @param string $path
*/
protected function writeNewConfiguration($file, $path)
{
list($prime, $inverse, $rand) = Energon::generate();
$file[] = "\nFAKEID_PRIME=" . $prime;
$file[] = "\nFAKEID_INVERSE=" . $inverse;
$file[] = "\nFAKEID_RANDOM=" . $rand;
file_put_contents($path, implode('', $file), LOCK_EX);
}
}
================================================
FILE: src/Facades/FakeId.php
================================================
publishes([
__DIR__ . '/../config/config.php' => config_path('fakeid.php'),
], 'config');
}
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->mergeConfigFrom(__DIR__ . '/../config/config.php', 'fakeid');
$this->registerCommand();
$this->registerOptimus();
}
/**
* Register the Optimus container.
*
* @return void
*/
protected function registerOptimus()
{
$this->app->singleton('Jenssegers\Optimus\Optimus', function ($app) {
return new Optimus(
$app['config']['fakeid.prime'],
$app['config']['fakeid.inverse'],
$app['config']['fakeid.random']
);
});
$this->app->alias('Jenssegers\Optimus\Optimus', 'optimus');
$this->app->alias('Jenssegers\Optimus\Optimus', 'fakeid');
}
/**
* Register the Artisan setup command.
*
* @return void
*/
protected function registerCommand()
{
$this->app->singleton('fakeid.command.setup', function ($app) {
return new FakeIdSetupCommand;
});
$this->commands('fakeid.command.setup');
}
}
================================================
FILE: src/RoutesWithFakeIds.php
================================================
getKey();
if ($this->getKeyType() === 'int' && (is_int($key) || ctype_digit($key))) {
return App::make('fakeid')->encode((int) $key);
}
throw new RuntimeException('Key should be of type int to encode into a fake id.');
}
/**
* Retrieve the model for a bound value.
*
* @param \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Relations\Relation $query
* @param mixed $value
* @param string|null $field
* @return \Illuminate\Database\Eloquent\Builder
*/
public function resolveRouteBindingQuery($query, $value, $field = null)
{
if (ctype_digit($value) || is_int($value)) {
try {
$value = App::make('fakeid')->decode((int) $value);
} catch (Exception $e) {}
}
return $query->where($field ?? $this->getRouteKeyName(), $value);
}
}
================================================
FILE: tests/Entities/Deletable.php
================================================
configureDatabase();
}
protected function configureDatabase()
{
$db = new DB;
$db->addConnection([
'driver' => 'sqlite',
'database' => ':memory:',
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '',
]);
$db->bootEloquent();
$db->setAsGlobal();
DB::schema()->create('reals', function ($table) {
$table->increments('id');
$table->timestamps();
});
DB::schema()->create('fakes', function ($table) {
$table->increments('id');
$table->timestamps();
});
DB::schema()->create('deletables', function ($table) {
$table->increments('id');
$table->timestamps();
$table->softDeletes();
});
}
protected function createRoute($path, $handler)
{
return $this->app['router']->get($path, [
'middleware' => SubstituteBindings::class,
'uses' => $handler,
]);
}
/** @test */
public function it_resolves_the_facade()
{
$this->assertInstanceOf('Jenssegers\Optimus\Optimus', FakeId::getFacadeRoot());
}
/** @test */
public function it_encodes_the_route_key()
{
$model = Fake::create();
$this->assertNotEquals($model->getRouteKey(), $model->getKey());
$this->assertEquals($model->getRouteKey(), app('fakeid')->encode($model->getKey()));
}
/** @test */
public function it_decodes_the_route_key_when_resolving()
{
$model = Fake::create();
$query = $model->resolveRouteBindingQuery(Fake::query(), $model->getRouteKey());
$this->assertNotEquals($model->getRouteKey(), $model->getKey());
$this->assertEquals('select * from "fakes" where "id" = ?', $query->toSql());
$this->assertEquals([$model->getKey()], $query->getBindings());
}
/** @test */
public function it_decodes_the_route_key_when_resolving_with_the_custom_route_key_name()
{
$model = FakeWithRouteKeyName::create();
$query = $model->resolveRouteBindingQuery(Fake::query(), $model->getRouteKey());
$this->assertNotEquals($model->getRouteKey(), $model->getKey());
$this->assertEquals('select * from "fakes" where "foo" = ?', $query->toSql());
$this->assertEquals([$model->getKey()], $query->getBindings());
}
/** @test */
public function it_decodes_the_route_key_when_resolving_with_a_custom_attribute()
{
$model = Fake::create();
$query = $model->resolveRouteBindingQuery(Fake::query(), $model->getRouteKey(), 'foo');
$this->assertNotEquals($model->getRouteKey(), $model->getKey());
$this->assertEquals('select * from "fakes" where "foo" = ?', $query->toSql());
$this->assertEquals([$model->getKey()], $query->getBindings());
}
/** @test */
public function it_doesnt_throw_when_resolving_an_undecodable_route_key()
{
$model = Fake::create();
$query = $model->resolveRouteBindingQuery(Fake::query(), 'abc');
$this->assertNotEquals($model->getRouteKey(), $model->getKey());
$this->assertEquals('select * from "fakes" where "id" = ?', $query->toSql());
$this->assertEquals(['abc'], $query->getBindings());
}
/** @test */
public function it_resolves_implicit_bindings()
{
$this->createRoute('fake/{fake}', function (Fake $fake) {
return "ID:{$fake->getKey()}";
});
$model = Fake::create();
$response = $this->get("fake/{$model->getRouteKey()}");
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals("ID:{$model->getKey()}", $response->getContent());
}
/** @test */
public function it_resolves_implicit_bindings_with_trashed()
{
$this->createRoute('fake/{deletable}', function (Deletable $deletable) {
return "ID:{$deletable->getKey()}";
})->withTrashed();
$model = Deletable::create();
$model->delete();
$response = $this->get("fake/{$model->getRouteKey()}");
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals("ID:{$model->getKey()}", $response->getContent());
}
/** @test */
public function it_resolves_explicit_bindings()
{
Route::model('fake', Fake::class);
$this->createRoute('fake/{fake}', function (Fake $fake) {
return "ID:{$fake->getKey()}";
});
$model = Fake::create();
$response = $this->get("fake/{$model->getRouteKey()}");
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals("ID:{$model->getKey()}", $response->getContent());
}
/**
* Explicit model bindings completely omit a model's route resolution logic.
* `$route->withTrashed()` only works for implicit bindings, so it won't
* help us here. There's no real way to support this feature properly.
*
* This test solely exists to remind us all of that :-)
*
* Or if Laravel implements `withTrashed()` support for explicit
* bindings some time, it will notify us by simply failing.
*
* See next test for a working explicit binding callback.
*
* @test
*/
public function it_cannot_resolve_soft_deleted_explicit_bindings_with_trashed()
{
Route::model('deletable', Deletable::class);
$this->createRoute('fake/{deletable}', function (Deletable $deletable) {
return "ID:{$deletable->getKey()}";
})->withTrashed(); // Has NO effect for explicit bindings.
$model = Deletable::create();
$model->delete();
$response = $this->get("fake/{$model->getRouteKey()}");
$this->assertEquals(404, $response->getStatusCode());
$this->assertNotNull($response->exception);
$this->assertEquals(ModelNotFoundException::class, get_class($response->exception));
}
/**
* This test solely exists to provide a working boilerplate
* to showcase how explicit bindings could be configured
* to properly work with soft-deleted models.
*
* @test
*/
public function it_resolves_soft_deleted_explicit_bindings_with_trashed_with_working_callback()
{
// This is the important part.
Route::model('deletable', Deletable::class, function ($value) {
$query = Deletable::query()->withTrashed();
return (new Deletable)->resolveRouteBindingQuery($query, $value)->firstOrFail();
});
$this->createRoute('fake/{deletable}', function (Deletable $deletable) {
return "ID:{$deletable->getKey()}";
})->withTrashed(); // Has NO effect for explicit bindings.
$model = Deletable::create();
$model->delete();
$response = $this->get("fake/{$model->getRouteKey()}");
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals("ID:{$model->getKey()}", $response->getContent());
}
/** @test */
public function it_returns_notfound_on_model_not_found()
{
$this->createRoute('fake/{fake}', function (Fake $fake) {
return "ID:{$fake->getKey()}";
});
$model = Fake::create();
$model->delete();
$response = $this->get("fake/{$model->getRouteKey()}");
$this->assertEquals(404, $response->getStatusCode());
$this->assertNotNull($response->exception);
$this->assertEquals(ModelNotFoundException::class, get_class($response->exception));
}
/** @test */
public function it_returns_notfound_on_undecodable_route_key()
{
$this->createRoute('fake/{fake}', function (Fake $fake) {
return "ID:{$fake->getKey()}";
});
$response = $this->get('fake/foo');
$this->assertEquals(404, $response->getStatusCode());
$this->assertNotNull($response->exception);
$this->assertEquals(ModelNotFoundException::class, get_class($response->exception));
}
}