diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1798484 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +/vendor +/build +composer.lock +.phpunit.result.cache \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..297f785 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,17 @@ +language: php + +php: + - 7.3 + - 7.4 + - 8.0 + +env: + matrix: + - COMPOSER_FLAGS="--prefer-lowest" + - COMPOSER_FLAGS="" + +before_script: + - travis_retry composer update ${COMPOSER_FLAGS} + +script: + - vendor/bin/phpunit \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..82cba04 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog +All notable changes to this project will be documented in this file. + +## [Unreleased] + + +## [1.0.0] - 2021-08-14 +- Initial release \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..4d2feef --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Touhidur Rahman Abir + +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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0b99ebc --- /dev/null +++ b/README.md @@ -0,0 +1,141 @@ +# Laravel Model UUID + +A simple package to sanitize model data to create/update table records. + +## Installation + +Require the package using composer: + +```bash +composer require touhidurabir/laravel-model-sanitize +``` + +## What is does ? +The **Sanitize** package sanitize the passed **attributes** to proper model fillables at create or update. + +A model has multiple table schema based attributed associated with it. When we try to create a new model record or update an existing model record, we must provide the an array attributes that is propelry mapped to those arrtibute or table columns names . For example + +```php +$user = User::create([ + 'email' => 'somemail@test.com', + 'password' => Hash::make('password') +]); +``` + +The above code will run without any issue as both the **email** and **password** column presents in the users table . But for the following code + +```php +User::create([ + 'email' => 'somemail@test.com', + 'password' => 'password', + 'data' => 'some data' +]); +``` + +It will throw an **\Illuminate\Database\QueryException** if the **data** column not present in the users table. + +```bash +Illuminate\Database\QueryException with message 'SQLSTATE[HY000] [2002] Connection refused (SQL: insert into `users` (`email`, `password`, `updated_at`, `created_at`) values (somemail@test.com, password, 2021-08-23 10:15:25, 2021-08-23 10:15:25))' +``` + +The **Sanitize** package target to make it easier to handle such case as follow by including the **Sanitizable** trait in the models + +```php +$data = [ + 'email' => 'somemail@test.com', + 'password' => 'password', + 'data' => 'some data' +]; + +User::create($data); +``` +The above code will work if the **Sanitizable** trait is used in the **User** model class. it will sanitize the passed attributed to model fillables and table columns, thus removing the extra or non useable attributes from it . + +## How it will be helpful ? + +A great use case of this package is where one need to create multiple model instances from validated request data . For example + +```php +$validated = $request->validated(); + +$user = User::create($validated); + +$profile = $user->profile->create($validated); +``` +I personally use this appraoch in many of my laravel apps . + +## Usage + +Use the trait **Sanitizable** in model where uuid needed to attach + +```php +use Touhidurabir\ModelSanitize\Sanitizable; +use Illuminate\Database\Eloquent\Model; + +class User extends Model { + + use Sanitizable; +} +``` + +And thats all . it will automatically work for all the following methods +- **updateOrCreate** +- **firstOrCreate** +- **firstOrNew** +- **create** +- **forceCreate** +- **update** + +This package also includes some helper methods that can be used to handle the sanitization process manually. + +The **sanitize** static method will sanitize the given attributes list and retuen back the useable and valid attributes as an array + +```php +$data = [ + 'email' => 'somemail@test.com', + 'password' => 'password', + 'data' => 'some data', + 'name' => 'Test User' +]; + +User::sanitize($data); +``` + +This will return back as such : +```php +[ + 'email' => 'somemail@test.com', + 'password' => 'password', + 'name' => 'Test User' +] +``` + +The **gibberish** static method will sanitize the given attributes list and retuen back the gibberish/non userbale attributes as an array + +```php +$data = [ + 'email' => 'somemail@test.com', + 'password' => 'password', + 'data' => 'some data', + 'name' => 'Test User' +]; + +User::gibberish($data); +``` + +This will return back as such : +```php +[ + 'data' => 'some data', +] +``` + +The **sanitize** and **gibberish** methods can be used to check or manually sanitize and evaluate the in valid data that can be passed to create/update model records. + +## Contributing +Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. + +Please make sure to update tests as appropriate. + +## License +[MIT](./LICENSE.md) diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..d6f4e88 --- /dev/null +++ b/composer.json @@ -0,0 +1,43 @@ +{ + "name": "touhidurabir/laravel-model-sanitize", + "description": "A laravel package to handle sanitize process of model data to create/update model records.", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Touhidur Rahman", + "email": "abircse06@gmail.com" + } + ], + "require": { + "php": ">=7.2.0" + }, + "autoload" : { + "psr-4" : { + "Touhidurabir\\ModelSanitize\\": "src/" + } + }, + "autoload-dev" : { + "psr-4" : { + "Touhidurabir\\ModelSanitize\\Tests\\": "tests/" + } + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "orchestra/testbench": "^6.20", + "illuminate/support": "^8.54", + "illuminate/container": "^8.54", + "illuminate/database": "^8.54", + "illuminate/events": "^8.54" + }, + "extra": { + "laravel": { + "providers": [ + "Touhidurabir\\ModelSanitize\\ModelSanitizeServiceProvider" + ], + "aliases": { + "ModelUuid": "Touhidurabir\\ModelSanitize\\Facades\\ModelSanitize" + } + } + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..0071975 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,36 @@ + + + + + tests + + + + + src/ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Builder/SanitizableQueryBuilder.php b/src/Builder/SanitizableQueryBuilder.php new file mode 100644 index 0000000..f4a7cd0 --- /dev/null +++ b/src/Builder/SanitizableQueryBuilder.php @@ -0,0 +1,82 @@ +model->sanitizeToModelFillable($values)); + } + + + /** + * Get the first record matching the attributes or create it. + * + * @param array $attributes + * @param array $values + * @return \Illuminate\Database\Eloquent\Model|static + */ + public function firstOrCreate(array $attributes = [], array $values = []) + { + return parent::firstOrCreate($attributes, $this->model->sanitizeToModelFillable($values)); + } + + + /** + * Get the first record matching the attributes or instantiate it. + * + * @param array $attributes + * @param array $values + * @return \Illuminate\Database\Eloquent\Model|static + */ + public function firstOrNew(array $attributes = [], array $values = []) + { + return parent::firstOrNew($attributes, $this->model->sanitizeToModelFillable($values)); + } + + + /** + * Save a new model and return the instance. + * + * @param array $attributes + * @return \Illuminate\Database\Eloquent\Model|$this + */ + public function create(array $attributes = []) + { + return parent::create($this->model->sanitizeToModelFillable($attributes)); + } + + + /** + * Save a new model and return the instance. Allow mass-assignment. + * + * @param array $attributes + * @return \Illuminate\Database\Eloquent\Model|$this + */ + public function forceCreate(array $attributes) + { + return parent::forceCreate($this->model->sanitizeToModelFillable($attributes)); + } + + + /** + * Update records in the database. + * + * @param array $values + * @return int + */ + public function update(array $values) + { + return parent::update($this->model->sanitizeToModelFillable($values)); + } +} \ No newline at end of file diff --git a/src/Facades/ModelSanitize.php b/src/Facades/ModelSanitize.php new file mode 100644 index 0000000..882d539 --- /dev/null +++ b/src/Facades/ModelSanitize.php @@ -0,0 +1,18 @@ +getFillable(); + + $fillables = ! empty($fillable) + ? $fillable + : array_diff( + array_diff( + Schema::getColumnListing($this->getTable()), + $this->getGuarded() + ), + $this->getHidden() + ); + + return array_intersect_key($data, array_flip($fillables)); + } + + + /** + * Get the extra data that passed to model to create/update + * + * @param array $data + * @return array + */ + public function extraData(array $data) { + + $modelFillables = $this->sanitizeToModelFillable($data); + + return array_diff_key($data, $modelFillables); + } + + + /** + * Get the sanitized data/attributes for this model + * + * @param array $data + * @return array + */ + public static function sanitize(array $attributes = []) { + + return (new static)->sanitizeToModelFillable($attributes); + } + + + /** + * Get the gibberish data/attributes for this model + * + * @param array $data + * @return array + */ + public static function gibberish(array $attributes = []) { + + return (new static)->extraData($attributes); + } + + + /** + * Create a new Eloquent query builder for the model. + * + * @param \Illuminate\Database\Query\Builder $query + * @return \Touhidurabir\ModelSanitize\Builder\SanitizableQueryBuilder|static + */ + public function newEloquentBuilder($query) { + + return new SanitizableQueryBuilder($query); + } + + +} \ No newline at end of file diff --git a/tests/App/Profile.php b/tests/App/Profile.php new file mode 100644 index 0000000..8c9bd5b --- /dev/null +++ b/tests/App/Profile.php @@ -0,0 +1,30 @@ +getSchemaBuilder()->create('users', function (Blueprint $table) { + $table->increments('id'); + $table->string('email')->unique(); + $table->string('password', 60); + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + DB::connection()->getSchemaBuilder()->dropIfExists('users'); + } +} \ No newline at end of file diff --git a/tests/App/database/migrations/2014_10_12_000001_create_profiles_table.php b/tests/App/database/migrations/2014_10_12_000001_create_profiles_table.php new file mode 100644 index 0000000..fd27abc --- /dev/null +++ b/tests/App/database/migrations/2014_10_12_000001_create_profiles_table.php @@ -0,0 +1,33 @@ +getSchemaBuilder()->create('profiles', function (Blueprint $table) { + $table->increments('id'); + $table->string('first_name'); + $table->string('last_name'); + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + DB::connection()->getSchemaBuilder()->dropIfExists('profiles'); + } +} \ No newline at end of file diff --git a/tests/LaravelIntegrationTest.php b/tests/LaravelIntegrationTest.php new file mode 100644 index 0000000..d7a0453 --- /dev/null +++ b/tests/LaravelIntegrationTest.php @@ -0,0 +1,233 @@ + ModelSanitize::class, + ]; + } + + + /** + * Define environment setup. + * + * @param Illuminate\Foundation\Application $app + * @return void + */ + protected function defineEnvironment($app) { + + // Setup default database to use sqlite :memory: + $app['config']->set('database.default', 'testbench'); + $app['config']->set('database.connections.testbench', [ + 'driver' => 'sqlite', + 'database' => ':memory:', + 'prefix' => '', + ]); + + $app['config']->set('app.url', 'http://localhost/'); + $app['config']->set('app.debug', false); + $app['config']->set('app.key', env('APP_KEY', '1234567890123456')); + $app['config']->set('app.cipher', 'AES-128-CBC'); + } + + + /** + * Define database migrations. + * + * @return void + */ + protected function defineDatabaseMigrations() { + + $this->loadMigrationsFrom(__DIR__ . '/App/database/migrations'); + + $this->artisan('migrate', ['--database' => 'testbench'])->run(); + + $this->beforeApplicationDestroyed(function () { + $this->artisan('migrate:rollback', ['--database' => 'testbench'])->run(); + }); + } + + + /** + * @test + */ + public function models_using_sanitizable_trait_will_have_access_to_static_methods() { + + $this->assertTrue(method_exists('Touhidurabir\\ModelSanitize\\Tests\\App\\User', 'sanitize')); + $this->assertTrue(method_exists('Touhidurabir\\ModelSanitize\\Tests\\App\\User', 'gibberish')); + $this->assertTrue(method_exists('Touhidurabir\\ModelSanitize\\Tests\\App\\Profile', 'sanitize')); + $this->assertTrue(method_exists('Touhidurabir\\ModelSanitize\\Tests\\App\\Profile', 'gibberish')); + } + + + /** + * @test + */ + public function sanitize_method_will_return_array() { + + $this->assertIsArray(User::sanitize(['email' => 'somemail@mail.com', 'password' => 'password'])); + $this->assertIsArray(Profile::sanitize(['first_name' => 'first_name', 'last_name' => 'last_name'])); + } + + + /** + * @test + */ + public function gibberish_method_will_return_array() { + + $this->assertIsArray(User::gibberish(['email' => 'somemail@mail.com', 'data' => 'data'])); + $this->assertIsArray(Profile::sanitize(['first_name' => 'first_name', 'bio' => 'some bio'])); + } + + + /** + * @test + */ + public function sanitize_method_will_return_proper_attributes() { + + $this->assertEquals( + User::sanitize(['email' => 'somemail@mail.com', 'password' => 'password', 'data' => 'data']), + ['email' => 'somemail@mail.com', 'password' => 'password'] + ); + + $this->assertEquals( + Profile::sanitize(['first_name' => 'first_name', 'bio' => 'some bio']), + ['first_name' => 'first_name'] + ); + } + + + /** + * @test + */ + public function gibberish_method_will_return_non_useable_attributes() { + + $this->assertEquals( + User::gibberish(['email' => 'somemail@mail.com', 'password' => 'password', 'data' => 'data']), + ['data' => 'data'] + ); + + $this->assertEquals( + Profile::gibberish(['first_name' => 'first_name', 'bio' => 'some bio']), + ['bio' => 'some bio'] + ); + } + + + /** + * @test + */ + public function it_will_properly_filter_at_time_create() { + + $user = User::create(['email' => 'somemail@mail.com', 'password' => 'password', 'data' => 'data']); + $this->assertDatabaseHas('users', ['email' => 'somemail@mail.com', 'password' => 'password']); + + $profile = Profile::create(['first_name' => 'First Name', 'last_name' => 'Last _name', 'bio' => 'some bio']); + $this->assertDatabaseHas('profiles', ['first_name' => 'First Name', 'last_name' => 'Last _name']); + } + + + /** + * @test + */ + public function it_will_properly_filter_at_time_update() { + + $user = User::create(['email' => 'somemail@mail.com', 'password' => 'password']); + $user->update(['email' => 'newemail@test.com', 'data' => 'some data']); + + $this->assertDatabaseHas('users', ['email' => 'newemail@test.com']); + } + + + /** + * @test + */ + public function it_will_properly_sanitize_on_first_or_create() { + + $user = User::firstOrCreate(['email' => 'newmail001@mail.com'], ['email' => 'newmail001@mail.com', 'password' => 'password', 'data' => 'some new data']); + + $this->assertDatabaseHas('users', ['email' => 'newmail001@mail.com']); + } + + + /** + * @test + */ + public function it_will_properly_sanitize_on_first_or_new() { + + $user = user::firstOrNew(['email' => 'somemail@mail.com', 'password' => 'password', 'data' => 'some data']); + + $this->assertTrue($user instanceof User); + } + + + /** + * @test + */ + public function it_will_properly_sanitize_on_update_or_create() { + + $user = User::updateOrCreate(['email' => 'newtestmail001@test.mail'], [ + 'email' => 'newtestmail001@test.mail', + 'password' => 'password', + 'data' => 'some data' + ]); + + $this->assertDatabaseHas('users', ['email' => 'newtestmail001@test.mail']); + + $user = User::updateOrCreate(['email' => 'newtestmail001@test.mail'], [ + 'email' => 'updatednewtestmail001@test.mail', + 'password' => 'new_password', + 'data' => 'some data' + ]); + + $this->assertDatabaseHas('users', ['email' => 'updatednewtestmail001@test.mail']); + } + + + /** + * @test + */ + public function it_will_properly_sanitize_on_force_create() { + + $user = User::forceCreate([ + 'email' => 'newtestmail002@test.mail', + 'password' => 'password', + 'data' => 'some data' + ]); + + $this->assertDatabaseHas('users', ['email' => 'newtestmail002@test.mail']); + } + +} \ No newline at end of file