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 ![Tests](https://github.com/Propaganistas/Laravel-FakeId/workflows/Tests/badge.svg?branch=master) [![Latest Stable Version](https://poser.pugx.org/propaganistas/laravel-fakeid/v/stable)](https://packagist.org/packages/propaganistas/laravel-fakeid) [![Total Downloads](https://poser.pugx.org/propaganistas/laravel-fakeid/downloads)](https://packagist.org/packages/propaganistas/laravel-fakeid) [![License](https://poser.pugx.org/propaganistas/laravel-fakeid/license)](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)); } }