├── examples ├── database │ └── database.sqlite ├── Repositories │ ├── MinionRepository.php │ └── MissionRepository.php ├── Jobs │ └── MissionCreateJob.php ├── Requests │ ├── MinionCreateRequest.php │ ├── MinionUpdateRequest.php │ ├── MinionDeleteRequest.php │ └── MissionCreateRequest.php ├── Responses │ ├── file-upload-response.json │ ├── get-minions-paginated-response.json │ └── json-filters-applied-on-listing-api.json ├── routes │ └── api.php ├── Events │ └── BringMinionToLabEvent.php ├── migrations │ ├── 2020_04_25_193714_create_missions_table.php │ ├── 2020_04_25_193715_add_foreign_keys_to_missions_table.php │ ├── 2020_04_25_193727_create_minion_mission_mapping_table.php │ ├── 2020_04_24_175321_create_minions_table.php │ └── 2020_04_25_193728_add_foreign_keys_to_minion_mission_mapping_table.php ├── Controllers │ ├── MissionController.php │ └── MinionController.php ├── Models │ ├── MinionMissionMapping.php │ ├── Mission.php │ └── Minion.php ├── tests │ ├── suite │ │ ├── PostAPISuccessTest.php │ │ ├── GetAPISuccessTest.php │ │ ├── DeleteAPISuccessTest.php │ │ ├── IndexAPISuccessTest.php │ │ ├── PutAPISuccessTest.php │ │ └── FileAPISuccessTest.php │ └── TestCase.php └── Exceptions │ └── Handler.php ├── .DS_Store ├── .gitignore ├── src ├── .DS_Store ├── Files │ └── Services │ │ ├── AmazonS3Service.php │ │ ├── LocalFileUploadService.php │ │ └── SaveFileToS3Service.php ├── Requests │ └── BaseRequest.php ├── Contracts │ ├── IFile.php │ └── IBaseRepository.php ├── Exceptions │ ├── AppException.php │ ├── ForbiddenException.php │ ├── AuthorizationException.php │ ├── ValidationException.php │ ├── BusinessLogicException.php │ ├── ServiceNotImplementedException.php │ ├── InvalidCredentialsException.php │ └── Handler.php ├── Facades │ └── Laravelcore.php ├── Services │ ├── FileService.php │ ├── RequestService.php │ ├── Files │ │ ├── LocalFileUploadService.php │ │ ├── SaveFileToS3Service.php │ │ └── AmazonS3Service.php │ ├── EnvironmentService.php │ ├── UtilityService.php │ └── ImageService.php ├── Repositories │ ├── FileRepository.php │ └── EloquentBaseRepository.php ├── Jobs │ ├── Job.php │ └── BaseJob.php ├── config │ └── file.php ├── lang │ └── errors.php ├── Constants │ └── ErrorConstants.php ├── database │ └── migrations │ │ └── 2019_10_03_101111_create_files_table.php ├── Models │ └── File.php ├── Console │ └── Command │ │ └── FilesInitCommand.php ├── CoreServiceProvider.php ├── Http │ ├── Requests │ │ └── BaseRequest.php │ └── Controllers │ │ ├── FileController.php │ │ └── ApiController.php ├── resources │ └── views │ │ └── test.blade.php └── Rules │ └── RequestSanitizer.php ├── .editorconfig ├── phpunit.xml ├── .github └── pull_request_template.md ├── LICENSE ├── composer.json └── readme.md /examples/database/database.sqlite: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luezoidtechnologies/laravel-core/HEAD/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/* 2 | composer.lock 3 | .idea/* 4 | .phpunit.result.cache 5 | storage/* 6 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luezoidtechnologies/laravel-core/HEAD/src/.DS_Store -------------------------------------------------------------------------------- /src/Files/Services/AmazonS3Service.php: -------------------------------------------------------------------------------- 1 | 'required|string|min:3|max:255', 15 | 'totalEyes' => 'required|integer|min:0|max:2', 16 | 'favouriteSound' => 'present|nullable|string|max:255', 17 | 'hasHairs' => 'required|boolean' 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/Requests/MinionUpdateRequest.php: -------------------------------------------------------------------------------- 1 | 'required|string|min:3|max:255', 15 | 'totalEyes' => 'required|integer|min:0|max:2', 16 | 'favouriteSound' => 'present|nullable|string|max:255', 17 | 'hasHairs' => 'required|boolean' 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/Requests/MinionDeleteRequest.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'required', 18 | 'integer', 19 | RequestService::exists(Minion::class) 20 | ] 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/Responses/file-upload-response.json: -------------------------------------------------------------------------------- 1 | { 2 | "message": "File uploaded successfully", 3 | "data": { 4 | "type": "EXAMPLE", 5 | "name": "a61113f5afc5ad52eb59f98ce293c266.jpg", 6 | "localPath": "storage/examples/114f0e9c-cb8f-4585-9d70-63fc6eaccd19.jpg", 7 | "s3Key": null, 8 | "updatedAt": "2020-04-25T18:52:25.000000Z", 9 | "createdAt": "2020-04-25T18:52:25.000000Z", 10 | "id": 1, 11 | "url": "http://localhost:7872/storage/examples/114f0e9c-cb8f-4585-9d70-63fc6eaccd19.jpg" 12 | }, 13 | "type": null 14 | } 15 | -------------------------------------------------------------------------------- /src/Services/FileService.php: -------------------------------------------------------------------------------- 1 | fileRepository = $fileRepository; 21 | } 22 | 23 | public function create($name, $key, $type, $localPath) 24 | { 25 | return $this->fileRepository->create($type, $name, $key, $localPath); 26 | } 27 | 28 | 29 | } 30 | -------------------------------------------------------------------------------- /examples/routes/api.php: -------------------------------------------------------------------------------- 1 | ['minions' => 'id']]); 17 | Route::post('missions', 'MissionController@createMission')->name('missions.store'); 18 | -------------------------------------------------------------------------------- /src/Repositories/FileRepository.php: -------------------------------------------------------------------------------- 1 | type = $type; 24 | $file->name = $name; 25 | $file->local_path = $localPath; 26 | $file->s3_key = $s3Key; 27 | 28 | $file->save(); 29 | 30 | return $file; 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /examples/Repositories/MissionRepository.php: -------------------------------------------------------------------------------- 1 | true, // set this to false to use upload on S3 bucket 12 | 'temp_path' => 'uploads', 13 | 'aws_temp_link_time' => 10, 14 | 'types' => [ 15 | 'EXAMPLE' => [ 16 | 'type' => 'EXAMPLE', 17 | 'local_path' => 'storage/examples', 18 | 'bucket_name' => 'examples', // if files are gonna be uploaded on s3 bucket 19 | 'validation' => 'required', 20 | 'valid_file_types' => [ // define the extensions allowed 21 | 'csv', 22 | 'xls', 23 | 'xlsx', 24 | 'jpg', 25 | 'png' 26 | ], 27 | 'acl' => 'private' // for s3 bucket 28 | ] 29 | ] 30 | ]; -------------------------------------------------------------------------------- /examples/Events/BringMinionToLabEvent.php: -------------------------------------------------------------------------------- 1 | lead_by->name}' to Gru's Lab for mission '{$mission->name}' at your command at once.."); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/migrations/2020_04_25_193714_create_missions_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string('name'); 19 | $table->integer('lead_by_id')->unsigned()->index('lead_by_id'); 20 | $table->text('description', 65535)->nullable(); 21 | $table->timestamps(); 22 | }); 23 | } 24 | 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::drop('missions'); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /examples/migrations/2020_04_25_193715_add_foreign_keys_to_missions_table.php: -------------------------------------------------------------------------------- 1 | foreign('lead_by_id', 'missions_ibfk_1')->references('id')->on('minions')->onUpdate('CASCADE')->onDelete('RESTRICT'); 18 | }); 19 | } 20 | 21 | 22 | /** 23 | * Reverse the migrations. 24 | * 25 | * @return void 26 | */ 27 | public function down() 28 | { 29 | Schema::table('missions', function (Blueprint $table) { 30 | $table->dropForeign('missions_ibfk_1'); 31 | }); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /examples/Controllers/MissionController.php: -------------------------------------------------------------------------------- 1 | customRequest = MissionCreateRequest::class; 23 | return $this->handleCustomEndPoint(MissionCreateJob::class, $request); // Calling custom handler function with a Custom Job specifically created to trigger an Event 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./examples/tests/suite 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /examples/Controllers/MinionController.php: -------------------------------------------------------------------------------- 1 | "Internal Server Error", 11 | 12 | 'type_validation_error' => 'VALIDATION', 13 | 'type_authorization_error' => 'AUTHORIZATION', 14 | 'type_invalid_token_error' => 'INVALID_TOKEN', 15 | 'type_invalid_credentials_error' => 'INVALID_CREDENTIALS', 16 | 'type_expired_token_error' => 'EXPIRED_TOKEN', 17 | 'type_method_not_allowed_error' => 'METHOD_NOT_ALLOWED', 18 | 'type_resource_not_found_error' => 'RESOURCE_NOT_FOUND', 19 | 'type_forbidden_error' => 'FORBIDDEN_ERROR', 20 | 'type_internal_server_error' => 'INTERNAL_SERVER_ERROR', 21 | 'type_service_not_implemented_error' => 'SERVICE_NOT_IMPLEMENTED', 22 | 'type_business_logic_error' => 'BUSINESS_LOGIC_ERROR', 23 | 24 | 'user_not_permitted_to_login' => 'user not permitted to login', 25 | 'user_not_found' => 'user not found' 26 | ]; -------------------------------------------------------------------------------- /src/Constants/ErrorConstants.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->string('name'); 19 | $table->string('s3_key')->nullable(); 20 | $table->string('local_path')->nullable(); 21 | $table->string('type')->index(); 22 | $table->softDeletes(); 23 | $table->timestamps(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('files'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/migrations/2020_04_25_193727_create_minion_mission_mapping_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->integer('minion_id')->unsigned()->index('minion_id'); 19 | $table->integer('mission_id')->unsigned()->index('mission_id'); 20 | $table->timestamps(); 21 | $table->unique(['minion_id', 'mission_id'], 'minion_mission_unique'); 22 | }); 23 | } 24 | 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::drop('minion_mission_mapping'); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Type of change 2 | 3 | Please choose appropriate options. 4 | 5 | - [ ] Bug fix (non-breaking change which fixes an issue) 6 | - [ ] New feature (non-breaking change which adds functionality) 7 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 8 | - [ ] This change requires a documentation update 9 | 10 | **Test Configuration**: 11 | * Software versions: Laravel 8, php 8 12 | * Hardware versions: 13 | 14 | ### Checklist: 15 | 16 | - [ ] My code follows the style guidelines of this project 17 | - [ ] I have performed a self-review of my own code 18 | - [ ] I have commented my code, particularly in hard-to-understand areas 19 | - [ ] I have made corresponding changes to the documentation 20 | - [ ] My changes generate no new warnings 21 | - [ ] I have added tests that prove my fix is effective or that my feature works 22 | - [ ] Any dependent changes have been merged and published in downstream modules 23 | 24 | 25 | ### Details of PR 26 | 27 | Write details of PR 28 | -------------------------------------------------------------------------------- /examples/Requests/MissionCreateRequest.php: -------------------------------------------------------------------------------- 1 | ['required', 21 | 'string', 22 | 'min:5', 23 | 'max:255', 24 | RequestService::unique(Mission::class, 'name') // Mission name has to be unique 25 | ], 26 | 'description' => 'present|nullable|min:10|max:1000', 27 | 'minionId' => [ 28 | 'required', 29 | 'integer', 30 | RequestService::exists(Minion::class) // Minion must exists 31 | ] 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Models/File.php: -------------------------------------------------------------------------------- 1 | type]; 24 | $acl = $type['acl'] ?? 'private'; 25 | if ($this->local_path) { 26 | return url($this->local_path); 27 | } else { 28 | $amazonS3Service = app()->make(AmazonS3Service::class); 29 | return $acl === 'private' ? 30 | $amazonS3Service->getSignedUrl( 31 | $this->s3_key, $type['bucket_name'] 32 | ) : $amazonS3Service->getObjectUrl( 33 | $this->s3_key, $type['bucket_name'] 34 | ); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /examples/migrations/2020_04_24_175321_create_minions_table.php: -------------------------------------------------------------------------------- 1 | increments('id')->unsigned(); 18 | $table->string('name', 255); 19 | $table->unsignedSmallInteger('total_eyes'); 20 | $table->string('favourite_sound', 255); 21 | $table->boolean('has_hairs')->default(false); 22 | $table->timestamps(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::table('minions', function (Blueprint $table) { 34 | $table->drop(); 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/Models/MinionMissionMapping.php: -------------------------------------------------------------------------------- 1 | 'int', 32 | 'mission_id' => 'int' 33 | ]; 34 | 35 | protected $fillable = [ 36 | 'minion_id', 37 | 'mission_id' 38 | ]; 39 | 40 | public function minion() 41 | { 42 | return $this->belongsTo(Minion::class); 43 | } 44 | 45 | public function mission() 46 | { 47 | return $this->belongsTo(Mission::class); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/tests/suite/PostAPISuccessTest.php: -------------------------------------------------------------------------------- 1 | 'Stuart', 17 | 'totalEyes' => 2, 18 | 'favouriteSound' => 'Grrrrrrrrrrr', 19 | 'hasHairs' => true, 20 | ]; 21 | 22 | $response = $this->postJson('/api/minions', $payload); 23 | $response->assertStatus(200); 24 | $response->assertJson([ 25 | 'message' => 'Resource Created successfully', 26 | 'data' => [ 27 | 'name' => 'Stuart', 28 | 'totalEyes' => 2, 29 | 'favouriteSound' => 'Grrrrrrrrrrr', 30 | 'hasHairs' => true, 31 | 'id' => 1, 32 | ], 33 | 'type' => null, 34 | ]); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Luezoid Technologies 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Services/Files/LocalFileUploadService.php: -------------------------------------------------------------------------------- 1 | fileService = $fileService; 19 | } 20 | 21 | public function save($name, $file, $type, $is_uuid_file_name_enabled = null, $acl = null) 22 | { 23 | $extensionArray = explode('.', $name); 24 | $extension = $extensionArray[count($extensionArray) - 1]; 25 | $fileName = $is_uuid_file_name_enabled ? Uuid::uuid4() . "." . $extension : $name; 26 | $destination = config('file.types')[$type]['local_path']; 27 | $isFileMoved = $file->move($destination, $fileName); 28 | if (!$isFileMoved) 29 | throw new AppException("File not moved"); 30 | 31 | return $this->fileService->create($name, null, $type, $destination . '/' . $fileName); 32 | 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/migrations/2020_04_25_193728_add_foreign_keys_to_minion_mission_mapping_table.php: -------------------------------------------------------------------------------- 1 | foreign('minion_id', 'minion_mission_mapping_ibfk_1')->references('id')->on('minions')->onUpdate('CASCADE')->onDelete('RESTRICT'); 18 | $table->foreign('mission_id', 'minion_mission_mapping_ibfk_2')->references('id')->on('missions')->onUpdate('CASCADE')->onDelete('RESTRICT'); 19 | }); 20 | } 21 | 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Schema::table('minion_mission_mapping', function (Blueprint $table) { 31 | $table->dropForeign('minion_mission_mapping_ibfk_1'); 32 | $table->dropForeign('minion_mission_mapping_ibfk_2'); 33 | }); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /examples/Models/Mission.php: -------------------------------------------------------------------------------- 1 | 'int' 34 | ]; 35 | 36 | protected $fillable = [ 37 | 'name', 38 | 'lead_by_id', 39 | 'description' 40 | ]; 41 | 42 | public function lead_by() 43 | { 44 | return $this->belongsTo(Minion::class, 'lead_by_id'); 45 | } 46 | 47 | public function minions() 48 | { 49 | return $this->belongsToMany(Minion::class, 'minion_mission_mapping') 50 | ->withPivot('id') 51 | ->withTimestamps(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/Models/Minion.php: -------------------------------------------------------------------------------- 1 | 'int', 43 | 'has_hairs' => 'bool' 44 | ]; 45 | protected $fillable = [ 46 | 'name', 47 | 'total_eyes', 48 | 'favourite_sound', 49 | 'has_hairs' 50 | ]; 51 | 52 | public function leading_mission() 53 | { 54 | return $this->hasOne(Mission::class, 'lead_by_id'); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/tests/suite/GetAPISuccessTest.php: -------------------------------------------------------------------------------- 1 | 'Stuart', 18 | 'totalEyes' => 2, 19 | 'favouriteSound' => 'Grrrrrrrrrrr', 20 | 'hasHairs' => true, 21 | ]; 22 | // Create a minion. 23 | $this->postJson('/api/minions', $payload); 24 | 25 | // Make the request. 26 | $response = $this->getJson('/api/minions/1'); 27 | 28 | // Assert that the response is successful. 29 | $response->assertOk(); 30 | 31 | // Assert that the response data is correct. 32 | $response->assertJson([ 33 | 'message' => null, 34 | 'data' => [ 35 | 'id' => 1, 36 | 'name' => 'Stuart', 37 | 'totalEyes' => 2, 38 | 'favouriteSound' => 'Grrrrrrrrrrr', 39 | 'hasHairs' => true 40 | ], 41 | 'type' => null, 42 | ]); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Console/Command/FilesInitCommand.php: -------------------------------------------------------------------------------- 1 | $config) { 42 | if (isset($config['local_path']) && !file_exists(public_path() . '/' . $config['local_path'])) { 43 | mkdir(public_path() . '/' . $config['local_path'], 0775, true); 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/Exceptions/Handler.php: -------------------------------------------------------------------------------- 1 | 'Stuart', 19 | 'totalEyes' => 2, 20 | 'favouriteSound' => 'Grrrrrrrrrrr', 21 | 'hasHairs' => true, 22 | ]; 23 | // Create a minion. 24 | $this->postJson('/api/minions', $payload); 25 | 26 | // Make the request. 27 | $response = $this->deleteJson('/api/minions/1'); 28 | // Assert that the response is successful. 29 | $response->assertOk(); 30 | // Assert that the minion is deleted. 31 | $response->assertJson([ 32 | 'message' =>"Resource deleted successfully", 33 | 'data' => [ 34 | 'id' => 1, 35 | 'name' => 'Stuart', 36 | 'totalEyes' => 2, 37 | 'favouriteSound' => 'Grrrrrrrrrrr', 38 | 'hasHairs' => true 39 | ], 40 | 'type' => null, 41 | ]); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/CoreServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadTranslationsFrom(__DIR__ . '/lang/errors.php', 'errors'); 24 | $this->loadMigrationsFrom(__DIR__ . '/database/migrations/2019_10_03_101111_create_files_table.php'); 25 | } 26 | 27 | /** 28 | * Register the application services. 29 | * 30 | * @return void 31 | */ 32 | public function register() 33 | { 34 | $this->mergeConfigFrom( 35 | __DIR__ . '/config/file.php', 'file' 36 | ); 37 | 38 | 39 | $this->publishes([ 40 | __DIR__ . '/config/file.php' => config_path('file.php'), 41 | ], 'luezoid-file-config'); 42 | $this->app->alias(Laravelcore::class, 'luezoid-core'); 43 | $this->app->alias(\Aws\Laravel\AwsFacade::class, 'AWS'); 44 | $this->app->register(\Aws\Laravel\AwsServiceProvider::class); 45 | $this->commands($this->commands); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Services/EnvironmentService.php: -------------------------------------------------------------------------------- 1 | user(); 37 | self::$loggedInUserId = self::$loggedInUser->id ?? null; 38 | } catch (\Exception $exception) { 39 | // declaring the variables as null as they are already loaded 40 | self::$loggedInUserId = self::$loggedInUser = null; 41 | } finally { 42 | self::$isLoaded = true; 43 | } 44 | } 45 | 46 | /** 47 | * @return null|object 48 | */ 49 | public static function getLoggedInUser() 50 | { 51 | if (!self::$isLoaded) { 52 | self::load(); 53 | } 54 | return self::$loggedInUser; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/tests/suite/IndexAPISuccessTest.php: -------------------------------------------------------------------------------- 1 | 'Stuart', 18 | 'totalEyes' => 2, 19 | 'favouriteSound' => 'Grrrrrrrrrrr', 20 | 'hasHairs' => true, 21 | ]; 22 | // Create a minion. 23 | $this->postJson('/api/minions', $payload); 24 | 25 | // Make the request. 26 | $response = $this->getJson('/api/minions'); 27 | 28 | $response->assertOk(); 29 | 30 | // Assert that the response data is correct. 31 | $response->assertJson([ 32 | 'message' => null, 33 | 'data' => [ 34 | 'items' => [ 35 | [ 36 | 'id' => 1, 37 | 'name' => 'Stuart', 38 | 'totalEyes' => 2, 39 | 'favouriteSound' => 'Grrrrrrrrrrr', 40 | 'hasHairs' => true 41 | ] 42 | ], 43 | 'page' => 1, 44 | 'total' => 1, 45 | 'pages' => 1, 46 | 'perpage' => 15, 47 | ], 48 | 'type' => null, 49 | ]); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/tests/suite/PutAPISuccessTest.php: -------------------------------------------------------------------------------- 1 | postJson('/api/minions', [ 19 | 'name' => 'Stuart', 20 | 'totalEyes' => 2, 21 | 'favouriteSound' => 'Grrrrrrrrrrr', 22 | 'hasHairs' => true, 23 | ]); 24 | 25 | // Prepare the request payload. 26 | $payload = [ 27 | 'name' => 'Stuart', 28 | 'totalEyes' => 2, 29 | 'favouriteSound' => 'Hrrrrrrrrrrr', 30 | 'hasHairs' => true, 31 | ]; 32 | 33 | // Make the request. 34 | $response = $this->putJson('/api/minions/1', $payload); 35 | 36 | // Assert that the response is successful. 37 | $response->assertOk(); 38 | 39 | // Assert that the response data is correct. 40 | $response->assertJson([ 41 | 'message' => 'Resource Updated successfully', 42 | 'data' => [ 43 | 'id' => 1, 44 | 'name' => 'Stuart', 45 | 'totalEyes' => 2, 46 | 'favouriteSound' => 'Hrrrrrrrrrrr', 47 | 'hasHairs' => true 48 | ], 49 | 'type' => null, 50 | ]); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Services/Files/SaveFileToS3Service.php: -------------------------------------------------------------------------------- 1 | amazonS3Service = $amazonS3Service; 25 | $this->fileService = $fileService; 26 | } 27 | 28 | 29 | public function save($name, $file, $type, $is_uuid_file_name_enabled = null, $acl = null) 30 | { 31 | $fileExtension = $file->getClientOriginalExtension(); 32 | //Decide bucket with type, we'll keep separate buckets for documents and profile images 33 | 34 | $bucketName = config('file.types')[$type]['bucket_name']; 35 | $s3Key = $this->amazonS3Service->saveFileToAmazonServer($file->getRealPath(), 36 | $is_uuid_file_name_enabled ? Uuid::uuid4() . "." . $fileExtension : $name, $bucketName, $acl 37 | ? $acl : config('file.types')[$type]['acl'] ?? "private"); 38 | if (!$s3Key) { 39 | //TODO: take message from lang 40 | throw new AppException("There is some problem in saving file to s3", 500); 41 | } 42 | return $this->fileService->create($name, $s3Key, $type, null); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/tests/TestCase.php: -------------------------------------------------------------------------------- 1 | > 23 | */ 24 | protected function getPackageProviders($app): array 25 | { 26 | return [ 27 | 'Luezoid\Laravelcore\CoreServiceProvider', 28 | ]; 29 | } 30 | 31 | public function defineEnvironment($app) 32 | { 33 | tap($app->make('config'), function (Repository $config) { 34 | $config->set('database.default', 'test'); 35 | $config->set('database.connections.test', [ 36 | 'driver' => 'sqlite', 37 | 'database' => ':memory:', 38 | 'prefix' => '', 39 | ]); 40 | }); 41 | } 42 | 43 | protected function defineRoutes($router) 44 | { 45 | Route::resource('api/minions', MinionController::class, ['parameters' => ['minions' => 'id']]); 46 | Route::post('api/files',[FileController::class, 'store']); 47 | } 48 | 49 | protected function defineDatabaseMigrations() 50 | { 51 | $this->loadMigrationsFrom(__DIR__ . '/../migrations'); 52 | $this->artisan('migrate', ['--database' => 'test'])->run(); 53 | } 54 | 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/Http/Requests/BaseRequest.php: -------------------------------------------------------------------------------- 1 | json()->all(), 34 | $this->all(), 35 | $this->route()->parameters() 36 | ); 37 | $this->inputs = array_merge($this->inputs, $inputs); 38 | return $this->inputs; 39 | } 40 | 41 | /** 42 | * Handle a failed validation attempt. 43 | * 44 | * @param \Illuminate\Contracts\Validation\Validator $validator 45 | * @return mixed 46 | */ 47 | public function getValidator() 48 | { 49 | return $this->getValidatorInstance(); 50 | } 51 | 52 | public function getEnvironmentId() 53 | { 54 | return $this->header('env_id', null); 55 | } 56 | 57 | /** 58 | * Add extra variable(s) in the input request data. Can be used in any child Request Class. 59 | * @param $array 60 | */ 61 | protected function add($array) 62 | { 63 | $this->inputs = array_merge($this->inputs, $array); 64 | } 65 | 66 | protected function failedValidation(\Illuminate\Contracts\Validation\Validator $validator) 67 | { 68 | $this->validator = $validator; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Jobs/BaseJob.php: -------------------------------------------------------------------------------- 1 | data = $data; 69 | } 70 | 71 | /** 72 | * Execute the job. 73 | * 74 | * @return mixed 75 | */ 76 | public function handle() 77 | { 78 | $repository = new $this->repository; 79 | $item = $repository->{$this->method}($this->data); 80 | 81 | if ($this->event) { 82 | event(new $this->event($item)); 83 | } 84 | 85 | if ($this->transformer) { 86 | $transformer = app()->make($this->transformer); 87 | if (!empty($this->dataToFetch)) { 88 | $transformer->setPropertiesToFetch($this->dataToFetch); 89 | } 90 | return $transformer->{$this->transformerMethod}($item); 91 | } 92 | return $item; 93 | } 94 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "luezoid/laravel-core", 3 | "description": "A feature rich Laravel package which provides fast & flexible way to quickly build powerful RESTful APIs. Various other features like queries & filters over nested complex relationships between models can be done on the go using this package. Read the docs for more info.", 4 | "keywords": [ 5 | "laravel", 6 | "laravel-package", 7 | "luezoid", 8 | "luezoid-laravel-core", 9 | "api" 10 | ], 11 | "license": "MIT", 12 | "support": { 13 | "issues": "https://github.com/luezoidtechnologies/laravel-core/issues", 14 | "source": "https://github.com/luezoidtechnologies/laravel-core" 15 | }, 16 | "authors": [ 17 | { 18 | "name": "Keshav Ashta", 19 | "email": "keshav@luezoid.com" 20 | }, 21 | { 22 | "name": "Abhishek Mishra", 23 | "email": "abhishek@luezoid.com" 24 | }, 25 | { 26 | "name": "Manoj Singh Rawat", 27 | "email": "manoj@luezoid.com" 28 | } 29 | ], 30 | "require": { 31 | "illuminate/support": ">=5.1", 32 | "intervention/image": "^2.4", 33 | "aws/aws-sdk-php-laravel": "^3.1", 34 | "ext-json": "*" 35 | }, 36 | "require-dev": { 37 | "laravel/framework": "^5.0|^6.0|^7.0|^8.0|^9.0|^10.0", 38 | "orchestra/testbench": "^8.5", 39 | "phpunit/phpunit": "^10.1", 40 | "ext-gd": "*", 41 | "ext-sqlite3": "*" 42 | }, 43 | "autoload": { 44 | "psr-4": { 45 | "Luezoid\\Laravelcore\\": "src/" 46 | } 47 | }, 48 | "extra": { 49 | "laravel": { 50 | "providers": [ 51 | "Luezoid\\Laravelcore\\CoreServiceProvider" 52 | ], 53 | "aliases": { 54 | "luezoid-laravel-core": "Luezoid\\Laravelcore\\Facades\\Laravelcore" 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /examples/tests/suite/FileAPISuccessTest.php: -------------------------------------------------------------------------------- 1 | image('test-image.jpg'); // Create a fake test file 28 | 29 | $this->app->bind(IFile::class, function ($app) { 30 | if (config('file.is_local')) { 31 | return $app->make(LocalFileUploadService::class); 32 | } 33 | return $app->make(SaveFileToS3Service::class); 34 | }); 35 | 36 | $response = $this->post('/api/files', [ 37 | 'file' => $file, 38 | 'type' => 'EXAMPLE', 39 | ]); 40 | 41 | $response->assertStatus(200); // Assert that the response has a status code of 200 42 | // Assert the JSON structure of the response 43 | $response->assertJsonStructure([ 44 | 'message', 45 | 'data' => [ 46 | 'type', 47 | 'name', 48 | 'localPath', 49 | 's3Key', 50 | 'updatedAt', 51 | 'createdAt', 52 | 'id', 53 | 'url', 54 | ], 55 | 'type', 56 | ]); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/resources/views/test.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Laravel 9 | 10 | 11 | 12 | 13 | 14 | 66 | 67 | 68 |
69 | 70 |
71 | 74 | 75 |
76 |

77 | {{\Luezoid\Laravelcore\Test::saySomething()}} 78 |

79 | Config message: {{ config('test.message') }} 80 |

81 |
82 |
83 |
84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /src/Services/Files/AmazonS3Service.php: -------------------------------------------------------------------------------- 1 | createClient('s3'); 18 | $s3->putObject(array( 19 | 'Bucket' => $bucketName, 20 | 'Key' => $key, 21 | 'ACL' => $aclType, 22 | 'SourceFile' => $filePath, 23 | )); 24 | return $key; 25 | 26 | } catch (\Exception $e) { 27 | throw new AppException($e); 28 | } 29 | } 30 | 31 | 32 | public function getBucketsList() 33 | { 34 | $s3 = App::make('aws')->createClient('s3'); 35 | return $s3->listBuckets(); 36 | 37 | } 38 | 39 | public function getTemporaryLink($key, $bucketName, $time) 40 | { 41 | $s3 = App::make('aws')->createClient('s3'); 42 | return $s3->getObjectUrl($bucketName, $key, $time); 43 | 44 | } 45 | 46 | public function getSignedUrl($key, $bucketName, $time = null) 47 | { 48 | $time = $time ? $time : config('file.aws_temp_link_ime') ?? 15; 49 | if ($time / (60 * 24 * 7) >= 1) { 50 | throw new AppException("Time should be less than 7 days"); 51 | } 52 | $s3 = App::make('aws')->createClient('s3'); 53 | $cmd = $s3->getCommand('GetObject', [ 54 | 'Bucket' => $bucketName, 55 | 'Key' => $key 56 | ]); 57 | 58 | $request = $s3->createPresignedRequest($cmd, "+ $time minutes"); 59 | 60 | return (string)$request->getUri(); 61 | 62 | } 63 | 64 | public function getObjectUrl($key, $bucketName) 65 | { 66 | $s3 = App::make('aws')->createClient('s3'); 67 | return $s3->getObjectUrl( 68 | $bucketName, 69 | $key 70 | ); 71 | } 72 | 73 | public function deleteFile($key, $bucketName) 74 | { 75 | $s3 = App::make('aws')->get('s3'); 76 | $result = $s3->deleteObject(array( 77 | 'Bucket' => $bucketName, 78 | 'Key' => $key 79 | )); 80 | return $result; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /examples/Responses/get-minions-paginated-response.json: -------------------------------------------------------------------------------- 1 | { 2 | "message": null, 3 | "data": { 4 | "items": [ 5 | { 6 | "id": 3, 7 | "name": "Hector", 8 | "totalEyes": 1, 9 | "favouriteSound": "Shhhhhhhhhhh", 10 | "hasHairs": false, 11 | "createdAt": "2020-04-25T14:55:26.000000Z", 12 | "updatedAt": "2020-04-27T14:55:26.000000Z" 13 | }, 14 | { 15 | "id": 2, 16 | "name": "Dave", 17 | "totalEyes": 1, 18 | "favouriteSound": "Hmmmmmmmmmmmmm Pchhhhhh", 19 | "hasHairs": true, 20 | "createdAt": "2020-04-25T14:55:04.000000Z", 21 | "updatedAt": "2020-04-25T14:55:04.000000Z" 22 | }, 23 | { 24 | "id": 7, 25 | "name": "Lucifer", 26 | "totalEyes": 0, 27 | "favouriteSound": "Luuuuuuu", 28 | "hasHairs": true, 29 | "createdAt": "2020-04-25T11:53:04.000000Z", 30 | "updatedAt": "2020-04-25T11:53:04.000000Z" 31 | }, 32 | { 33 | "id": 6, 34 | "name": "Khachhhh", 35 | "totalEyes": 2, 36 | "favouriteSound": "khhhhhuuuu", 37 | "hasHairs": true, 38 | "createdAt": "2020-04-25T11:52:39.000000Z", 39 | "updatedAt": "2020-04-25T11:52:39.000000Z" 40 | }, 41 | { 42 | "id": 5, 43 | "name": "RL Nuuuu", 44 | "totalEyes": 0, 45 | "favouriteSound": "Nuuuuuuuuuu", 46 | "hasHairs": true, 47 | "createdAt": "2020-04-25T11:52:13.000000Z", 48 | "updatedAt": "2020-04-25T11:52:13.000000Z" 49 | }, 50 | { 51 | "id": 4, 52 | "name": "AK Burrr", 53 | "totalEyes": 0, 54 | "favouriteSound": "Brrrrrrrrr", 55 | "hasHairs": true, 56 | "createdAt": "2020-04-25T11:51:47.000000Z", 57 | "updatedAt": "2020-04-25T11:51:47.000000Z" 58 | } 59 | ], 60 | "page": 1, 61 | "total": 6, 62 | "pages": 1, 63 | "perpage": 15 64 | }, 65 | "type": null 66 | } 67 | -------------------------------------------------------------------------------- /src/Services/UtilityService.php: -------------------------------------------------------------------------------- 1 | $input) { 34 | if (is_numeric($key)) { 35 | $newKey = $key; 36 | } else { 37 | $newKey = Str::snake($key); 38 | } 39 | unset($inputs[$key]); 40 | 41 | if (is_array($input)) { 42 | $inputs[$newKey] = self::fromCamelToSnake($input); 43 | } else { 44 | $inputs[$newKey] = $input; 45 | } 46 | } 47 | 48 | return $inputs; 49 | } 50 | 51 | public static function fromSnakeToCamel($inputs) 52 | { 53 | foreach ($inputs as $key => $input) { 54 | unset($inputs[$key]); 55 | $newKey = self::camel_case($key); 56 | if (is_array($input)) { 57 | $inputs[$newKey] = self::fromSnakeToCamel($input); 58 | } else { 59 | $inputs[$newKey] = $input; 60 | } 61 | } 62 | 63 | return $inputs; 64 | } 65 | 66 | public static function camel_case($key) 67 | { 68 | $newKey = $key; 69 | if (Str::contains($key, '_')) { 70 | $newKey = Str::camel($key); 71 | } 72 | return $newKey; 73 | } 74 | 75 | public static function getClassName($class) 76 | { 77 | $path = explode('\\', $class); 78 | return array_pop($path); 79 | 80 | } 81 | 82 | public static function getDays($dayName) 83 | { 84 | return new DatePeriod( 85 | Carbon::parse("first $dayName of this month"), 86 | CarbonInterval::week(), 87 | Carbon::parse("first $dayName of next month") 88 | ); 89 | } 90 | 91 | public static function getRequestSanitizerNotRequiredRule($validationData = []) 92 | { 93 | $timestamp = time(); 94 | return new RequestSanitizer($validationData, [ 95 | ['keyName' => "key$timestamp", 'type' => 'value', 'value' => $timestamp] 96 | ]); 97 | } 98 | 99 | public static function getModelTableName(string $modelClass) 100 | { 101 | $cacheKey = 'LZD_TABLE_' . $modelClass; 102 | return Cache::remember($cacheKey, 600, function () use ($modelClass) { 103 | return (new $modelClass)->getTable(); 104 | }); 105 | } 106 | 107 | public static function getColumnsForTable(string $modelClass) 108 | { 109 | $cacheKey = 'LZD_TABLE_COL_' . $modelClass; 110 | return Cache::remember($cacheKey, 600, function () use ($modelClass) { 111 | return Schema::getColumnListing(self::getModelTableName($modelClass)); 112 | }); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Rules/RequestSanitizer.php: -------------------------------------------------------------------------------- 1 | data = $data; 30 | $this->conditions = $conditions; 31 | $this->and = $and; 32 | } 33 | 34 | /** 35 | * Determine if the validation rule passes. 36 | * 37 | * @param string $attribute 38 | * @param mixed $value 39 | * @return bool 40 | */ 41 | public function passes($attribute, $value) 42 | { 43 | $this->message = "$attribute is not required."; 44 | if (isset($value)) { 45 | $isAllowedArray = []; 46 | 47 | foreach ($this->conditions as $index => $condition) { 48 | $isAllowedArray[$index] = false; 49 | if ($condition['type'] == '!value') { 50 | if (isset($this->data[$condition['keyName']]) && ($this->data[$condition['keyName']] != $condition['value'])) { 51 | $isAllowedArray[$index] = true; 52 | } 53 | } elseif ($condition['type'] == 'value') { 54 | if (isset($this->data[$condition['keyName']]) && ($this->data[$condition['keyName']] == $condition['value'] || (is_array($condition['value']) && in_array($this->data[$condition['keyName']], $condition['value'])))) { 55 | $isAllowedArray[$index] = true; 56 | } 57 | } else if ($condition['type'] == 'present') { 58 | if (is_array($condition['keyName'])) { 59 | 60 | foreach ($condition['keyName'] as $val) { 61 | if (isset($this->data[$val]) && !empty($this->data[$val])) { 62 | $isAllowedArray[$index] = true; 63 | break; 64 | } 65 | } 66 | } else { 67 | if (isset($this->data[$condition['keyName']]) && !empty($this->data[$condition['keyName']])) { 68 | $isAllowedArray[$index] = true; 69 | } 70 | } 71 | } 72 | } 73 | if ($this->and) { 74 | $isAllowed = null; 75 | foreach ($isAllowedArray as $value) { 76 | if (is_null($isAllowed)) { 77 | $isAllowed = $value; 78 | } else { 79 | $isAllowed = $isAllowed && $value; 80 | } 81 | } 82 | } else { 83 | $isAllowed = false; 84 | foreach ($isAllowedArray as $value) { 85 | if ($value == true) { 86 | $isAllowed = true; 87 | break; 88 | } 89 | } 90 | } 91 | return $isAllowed; 92 | } 93 | return true; 94 | } 95 | 96 | /** 97 | * Get the validation error message. 98 | * 99 | * @return string 100 | */ 101 | public function message() 102 | { 103 | return $this->message; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Http/Controllers/FileController.php: -------------------------------------------------------------------------------- 1 | fileService = $fileService; 34 | } 35 | 36 | public function store(Request $request) 37 | { 38 | $validation = ""; 39 | 40 | if (in_array($request->get('type'), array_keys(config('file.types'))) && config('file.types')[$request->get('type')] && isset(config('file.types')[$request->get('type')]['validation'])) { 41 | $validation = config('file.types')[$request->get('type')]['validation']; 42 | } 43 | 44 | $validator = [ 45 | 'type' => ['required', Rule::in(array_keys(config('file.types')))] 46 | ]; 47 | if ($validation) { 48 | $validator['file'] = $validation; 49 | } 50 | $data = $request->all(); 51 | if (isset(config('file.types')[$request->get('type')]['valid_file_types']) && $request->file) { 52 | $validator['extension'] = ['required', Rule::in(config('file.types')[$request->get('type')]['valid_file_types'])]; 53 | $data['extension'] = strtolower($request->file->getClientOriginalExtension()); 54 | } 55 | 56 | 57 | $validator = Validator::make($data, $validator); 58 | 59 | 60 | if ($validator->fails()) { 61 | return $this->standardResponse(null, $validator->errors()->messages(), 400, ErrorConstants::TYPE_BAD_REQUEST_ERROR); 62 | } 63 | 64 | $file = $request->file('file'); 65 | $fileType = $request->get('fileType', 'normal'); 66 | 67 | $type = $request->get('type'); 68 | if ($fileType == "base64") { 69 | 70 | $imageManager = new ImageManager(); 71 | $base64EncodedImageSource = $request->get('file'); 72 | 73 | $imageObject = $imageManager->make($base64EncodedImageSource); 74 | 75 | if ($imageObject->mime == 'image/jpeg') 76 | $extension = '.jpg'; 77 | elseif ($imageObject->mime == 'image/png') 78 | $extension = '.png'; 79 | elseif ($imageObject->mime == 'image/gif') 80 | $extension = '.gif'; 81 | else 82 | $extension = ''; 83 | 84 | $fileName = Uuid::uuid4() . $extension; 85 | $tempFilePath = public_path(config('file.temp_path')) . '/' . $fileName; 86 | 87 | $imageObject->save($tempFilePath, 100); 88 | $file = new UploadedFile($tempFilePath, $fileName); 89 | } else { 90 | $fileName = $request->get('fileName', $file->getClientOriginalName()); 91 | //TODO: file extension validation 92 | if (!$request->file('file')->isValid()) { 93 | Log::error(__('errors.errorInUploadingFile') . $request->file('file')->getError()); 94 | return $this->standardResponse(null, "Invalid File", 400, ErrorConstants::TYPE_VALIDATION_ERROR); 95 | } 96 | } 97 | 98 | 99 | $output = $this->fileService->save($fileName, $file, $type, config('file.save_uuid_file_name', true)); 100 | 101 | return $this->standardResponse($output, "File uploaded successfully"); 102 | } 103 | } -------------------------------------------------------------------------------- /src/Services/ImageService.php: -------------------------------------------------------------------------------- 1 | filesize(); 34 | $tempPath = sys_get_temp_dir() . pathinfo($imageFilePath, PATHINFO_BASENAME); 35 | 36 | if ($outputImagePath) { 37 | $tempPath = $outputImagePath; 38 | } 39 | 40 | if ($imageExtension == "png") { 41 | $this->compressPng($imageFilePath, $tempPath); 42 | } else { 43 | if ($imageExtension == "jpg") { 44 | $img->encode("jpg", 75); 45 | } else { 46 | $img->encode($imageExtension); 47 | } 48 | $img->save($tempPath); 49 | } 50 | 51 | $compressedImage = Image::make($tempPath); 52 | $compressedSize = $compressedImage->filesize(); 53 | 54 | if ($compressedSize < $originalSize && is_null($outputImagePath)) { 55 | copy($tempPath, $imageFilePath); 56 | } 57 | 58 | return $compressedImage; 59 | } 60 | 61 | /** 62 | * Compressing PNG files using PNG-QUANT 63 | * @param $pathToPngFile 64 | * @param $compressedFile 65 | * @param int $minQuality 66 | * @param int $maxQuality 67 | * @throws Exception 68 | */ 69 | function compressPng($pathToPngFile, $compressedFile, $minQuality = 60, $maxQuality = 90) 70 | { 71 | if (!file_exists($pathToPngFile)) { 72 | throw new Exception("File does not exist: $pathToPngFile"); 73 | } 74 | 75 | $command = "pngquant -f --quality=$minQuality-$maxQuality - < " . escapeshellarg($pathToPngFile) . " -o " . escapeshellarg($compressedFile); 76 | 77 | exec($command, $output, $result); 78 | if ($result !== 0) { 79 | throw new Exception("Conversion to compressed PNG failed. Is pngquant 1.8+ installed on the server?"); 80 | } 81 | 82 | return; 83 | } 84 | 85 | /** 86 | * @param $imageFilePath 87 | * @param $outputImagePath 88 | * @param $width 89 | * @param $height 90 | * @return 91 | * @throws Exception 92 | */ 93 | public function reSizeImage($imageFilePath, $outputImagePath, $width, $height) 94 | { 95 | if (!file_exists($imageFilePath)) { 96 | throw new Exception("File does not exist: $imageFilePath"); 97 | } 98 | if (is_null($width) && is_null($height)) { 99 | throw new Exception("Width and Height cannot be null"); 100 | } 101 | 102 | $img = Image::make($imageFilePath); 103 | 104 | // if width and height is present then resize image to fixed size 105 | // if width is given, Resize the image to given width and constrain aspect ratio (auto height) 106 | // if height is given, Resize the image to given height and constrain aspect ratio (auto width) 107 | if ($width && $height) { 108 | $img->resize($width, $height); 109 | } else if ($width) { 110 | $img->resize($width, null, function ($constraint) { 111 | $constraint->aspectRatio(); 112 | }); 113 | } else if ($height) { 114 | $img->resize(null, $height, function ($constraint) { 115 | $constraint->aspectRatio(); 116 | }); 117 | } 118 | 119 | $img->save($outputImagePath); 120 | 121 | return $img; 122 | } 123 | } -------------------------------------------------------------------------------- /examples/Responses/json-filters-applied-on-listing-api.json: -------------------------------------------------------------------------------- 1 | { 2 | "message": null, 3 | "data": { 4 | "items": [ 5 | { 6 | "id": 3, 7 | "name": "Hector", 8 | "favouriteSound": "Shhhhhhhhhhh", 9 | "missions": [ 10 | { 11 | "name": "Steal the Moon!", 12 | "description": "The first moon landing happened in 1969. Felonius Gru watched this historic moment with his mother and was inspired by the landing to go to outer space just like his idol Neil Armstrong.", 13 | "pivot": { 14 | "minionId": 3, 15 | "missionId": 1 16 | } 17 | }, 18 | { 19 | "name": "Steal the Moon! Part - 3", 20 | "description": "The first moon landing happened in 1969. Felonius Gru watched this historic moment with his mother and was inspired by the landing to go to outer space just like his idol Neil Armstrong.", 21 | "pivot": { 22 | "minionId": 3, 23 | "missionId": 3 24 | } 25 | } 26 | ], 27 | "leadingMission": null 28 | }, 29 | { 30 | "id": 2, 31 | "name": "Dave", 32 | "favouriteSound": "Hmmmmmmmmmmmmm Pchhhhhh", 33 | "missions": [ 34 | { 35 | "name": "Steal the Moon!", 36 | "description": "The first moon landing happened in 1969. Felonius Gru watched this historic moment with his mother and was inspired by the landing to go to outer space just like his idol Neil Armstrong.", 37 | "pivot": { 38 | "minionId": 2, 39 | "missionId": 1 40 | } 41 | }, 42 | { 43 | "name": "Steal the Moon! Part - 2", 44 | "description": "The first moon landing happened in 1969. Felonius Gru watched this historic moment with his mother and was inspired by the landing to go to outer space just like his idol Neil Armstrong.", 45 | "pivot": { 46 | "minionId": 2, 47 | "missionId": 2 48 | } 49 | } 50 | ], 51 | "leadingMission": { 52 | "id": 1, 53 | "name": "Steal the Moon!", 54 | "leadById": 2, 55 | "description": "The first moon landing happened in 1969. Felonius Gru watched this historic moment with his mother and was inspired by the landing to go to outer space just like his idol Neil Armstrong.", 56 | "createdAt": "2020-04-25T02:00:00.000000Z", 57 | "updatedAt": "2020-04-25T02:00:00.000000Z" 58 | } 59 | }, 60 | { 61 | "id": 7, 62 | "name": "Lucifer", 63 | "favouriteSound": "Luuuuuuu", 64 | "missions": [], 65 | "leadingMission": null 66 | }, 67 | { 68 | "id": 6, 69 | "name": "Khachhhh", 70 | "favouriteSound": "khhhhhuuuu", 71 | "missions": [], 72 | "leadingMission": null 73 | }, 74 | { 75 | "id": 5, 76 | "name": "RL Nuuuu", 77 | "favouriteSound": "Nuuuuuuuuuu", 78 | "missions": [], 79 | "leadingMission": null 80 | }, 81 | { 82 | "id": 4, 83 | "name": "AK Burrr", 84 | "favouriteSound": "Brrrrrrrrr", 85 | "missions": [ 86 | { 87 | "name": "Steal the Moon!", 88 | "description": "The first moon landing happened in 1969. Felonius Gru watched this historic moment with his mother and was inspired by the landing to go to outer space just like his idol Neil Armstrong.", 89 | "pivot": { 90 | "minionId": 4, 91 | "missionId": 1 92 | } 93 | }, 94 | { 95 | "name": "Steal the Moon! Part - 2", 96 | "description": "The first moon landing happened in 1969. Felonius Gru watched this historic moment with his mother and was inspired by the landing to go to outer space just like his idol Neil Armstrong.", 97 | "pivot": { 98 | "minionId": 4, 99 | "missionId": 2 100 | } 101 | }, 102 | { 103 | "name": "Steal the Moon! Part - 3", 104 | "description": "The first moon landing happened in 1969. Felonius Gru watched this historic moment with his mother and was inspired by the landing to go to outer space just like his idol Neil Armstrong.", 105 | "pivot": { 106 | "minionId": 4, 107 | "missionId": 3 108 | } 109 | }, 110 | { 111 | "name": "Steal the Moon! Part - 4", 112 | "description": "The first moon landing happened in 1969. Felonius Gru watched this historic moment with his mother and was inspired by the landing to go to outer space just like his idol Neil Armstrong.", 113 | "pivot": { 114 | "minionId": 4, 115 | "missionId": 4 116 | } 117 | } 118 | ], 119 | "leadingMission": null 120 | } 121 | ], 122 | "page": 1, 123 | "total": 6, 124 | "pages": 1, 125 | "perpage": 15 126 | }, 127 | "type": null 128 | } 129 | -------------------------------------------------------------------------------- /src/Exceptions/Handler.php: -------------------------------------------------------------------------------- 1 | appExceptionHandler($exception); 58 | } else if ($exception instanceof ValidationException) { 59 | $this->validationExceptionHandler($exception); 60 | } else if ($exception instanceof NotFoundHttpException) { 61 | $this->notFoundHttpExceptionHandler($exception); 62 | } else if ($exception instanceof MethodNotAllowedHttpException) { 63 | $this->methodNotAllowedHttpExceptionHandler($exception); 64 | } else if ($exception instanceof InvalidCredentialsException) { 65 | $this->invalidCredentialsExceptionHandler($exception); 66 | } else if ($exception instanceof AuthorizationException) { 67 | $this->authorizationExceptionHandler($exception); 68 | } else if ($exception instanceof ForbiddenException) { 69 | $this->forbiddenExceptionHandler($exception); 70 | } else if ($exception instanceof ServiceNotImplementedException) { 71 | $this->serviceNotImplementedExceptionHandler($exception); 72 | } else if ($exception instanceof BusinessLogicException) { 73 | $this->businessLogicExceptionHandler($exception); 74 | } else if ($exception instanceof BadRequestHttpException) { 75 | $this->badRequestHttpExceptionHandler($exception); 76 | } else if ($exception instanceof ThrottleRequestsException) { 77 | $this->throttleRequestsExceptionHandler($exception); 78 | } else { 79 | $this->exceptionHandler($exception); 80 | } 81 | 82 | $this->handleLogTrace(); 83 | 84 | return response()->json($this->errorResponse, $this->statusCode); 85 | } 86 | 87 | 88 | /** 89 | * Convert an authentication exception into an unauthenticated response. 90 | * 91 | * @param \Illuminate\Http\Request $request 92 | * @param \Illuminate\Auth\AuthenticationException $exception 93 | * @return \Illuminate\Http\Response 94 | */ 95 | protected function unauthenticated($request, AuthenticationException $exception) 96 | { 97 | if ($request->expectsJson()) { 98 | return response()->json(['error' => 'Unauthenticated.'], 401); 99 | } 100 | 101 | return redirect()->guest(route('login')); 102 | } 103 | 104 | public function appExceptionHandler($exception) 105 | { 106 | $this->statusCode = $exception->getCode(); 107 | $this->errorResponse = [ 108 | 'error' => !empty($exception->getMessage()) ? $exception->getMessage() : "Internal Server Error", 109 | 'type' => ErrorConstants::TYPE_INTERNAL_SERVER_ERROR, 110 | 'errorDetails' => $exception->getTrace() 111 | ]; 112 | } 113 | 114 | public function validationExceptionHandler($exception) 115 | { 116 | $errorDetails = []; 117 | $error = $exception->getMessage(); 118 | if (UtilityService::is_json($exception->getMessage())) { 119 | $err = json_decode($exception->getMessage()); 120 | $error = $err->error; 121 | $errorDetails = $err->errorDetails; 122 | } 123 | 124 | $this->statusCode = 400; 125 | $this->errorResponse = [ 126 | 'status' => 'fail', 127 | 'message' => array_merge([$error], $errorDetails), 128 | 'data' => null, 129 | 'type' => ErrorConstants::TYPE_VALIDATION_ERROR 130 | ]; 131 | } 132 | 133 | public function notFoundHttpExceptionHandler($exception) 134 | { 135 | $this->statusCode = 404; 136 | $this->errorResponse = [ 137 | 'error' => !empty($exception->getMessage()) ? $exception->getMessage() : "Resouce not found", 138 | 'type' => ErrorConstants::TYPE_RESOURCE_NOT_FOUND_ERROR, 139 | 'errorDetails' => "Resource not found" 140 | ]; 141 | } 142 | 143 | public function methodNotAllowedHttpExceptionHandler($exception) 144 | { 145 | $this->statusCode = 405; 146 | $this->errorResponse = [ 147 | 'error' => !empty($exception->getMessage()) ? $exception->getMessage() : "Method not allowed", 148 | 'type' => ErrorConstants::TYPE_METHOD_NOT_ALLOWED_ERROR, 149 | 'errorDetails' => "Method not allowed" 150 | ]; 151 | } 152 | 153 | public function invalidCredentialsExceptionHandler($exception) 154 | { 155 | $this->statusCode = 401; 156 | $this->errorResponse = [ 157 | 'error' => !empty($exception->getMessage()) ? $exception->getMessage() : "Invalid Credentials", 158 | 'type' => ErrorConstants::TYPE_INVALID_CREDENTIALS_ERROR, 159 | 'errorDetails' => $exception->getMessage() 160 | ]; 161 | } 162 | 163 | public function authorizationExceptionHandler($exception) 164 | { 165 | $this->statusCode = 403; 166 | $this->errorResponse = [ 167 | 'error' => !empty($exception->getMessage()) ? $exception->getMessage() : "Authorization Failed", 168 | 'type' => ErrorConstants::TYPE_AUTHORIZATION_ERROR, 169 | 'errorDetails' => $exception->getMessage() 170 | ]; 171 | } 172 | 173 | public function forbiddenExceptionHandler($exception) 174 | { 175 | $this->statusCode = 403; 176 | $this->errorResponse = [ 177 | 'error' => !empty($exception->getMessage()) ? $exception->getMessage() : "Forbidden from performing action", 178 | 'type' => ErrorConstants::TYPE_FORBIDDEN_ERROR, 179 | 'errorDetails' => $exception->getMessage() 180 | ]; 181 | } 182 | 183 | public function serviceNotImplementedExceptionHandler($exception) 184 | { 185 | $this->statusCode = 501; 186 | $this->errorResponse = [ 187 | 'error' => !empty($exception->getMessage()) ? $exception->getMessage() : "Service Not Implemented", 188 | 'type' => ErrorConstants::TYPE_SERVICE_NOT_IMPLEMENTED_ERROR, 189 | 'errorDetails' => $exception->getMessage() 190 | ]; 191 | } 192 | 193 | public function businessLogicExceptionHandler($exception) 194 | { 195 | $message = $exception->getMessage(); 196 | if (UtilityService::is_json($exception->getMessage())) { 197 | $message = json_decode($exception->getMessage()); 198 | } 199 | 200 | $this->statusCode = $exception->getCode(); 201 | $this->errorResponse = [ 202 | 'error' => !empty($message) ? $message : "Business Logic Error", 203 | 'errorDetails' => $exception->getMessage(), 204 | 'type' => ErrorConstants::TYPE_BUSINESS_LOGIC_ERROR 205 | ]; 206 | } 207 | 208 | public function badRequestHttpExceptionHandler($exception) 209 | { 210 | $this->statusCode = 400; 211 | $this->errorResponse = [ 212 | 'error' => !empty($exception->getMessage()) ? $exception->getMessage() : "Bad Request", 213 | 'type' => ErrorConstants::TYPE_BAD_REQUEST_ERROR, 214 | 'errorDetails' => "Bad Request" 215 | ]; 216 | } 217 | 218 | public function throttleRequestsExceptionHandler($exception) 219 | { 220 | $this->statusCode = 429; 221 | $this->errorResponse = [ 222 | 'error' => !empty($exception->getMessage()) ? $exception->getMessage() : "Bad Request", 223 | 'type' => ErrorConstants::TYPE_TOO_MANY_REQUEST_ERROR, 224 | 'errorDetails' => "Too many requests" 225 | ]; 226 | } 227 | 228 | public function exceptionHandler($exception) 229 | { 230 | $this->statusCode = 500; 231 | $this->errorResponse = [ 232 | 'error' => $exception->getMessage(), 233 | 'type' => ErrorConstants::TYPE_INTERNAL_SERVER_ERROR, 234 | 'errorDetails' => $exception->getTrace() 235 | ]; 236 | } 237 | 238 | public function handleLogTrace() 239 | { 240 | if (!env('APP_DEBUG')) { 241 | unset($this->errorResponse['errorDetails']); 242 | } 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/Http/Controllers/ApiController.php: -------------------------------------------------------------------------------- 1 | repository) { 80 | $this->repo = new $this->repository; 81 | 82 | if ($this->repo->model) { 83 | $this->model = ($this->repo)->model; 84 | } 85 | } 86 | } 87 | 88 | /** 89 | * global index function . return all data of Specific Model. 90 | * @param Request $request 91 | * @return \Illuminate\Http\JsonResponse 92 | * @throws \Exception 93 | */ 94 | public function index(Request $request) 95 | { 96 | $inputs = array_replace_recursive( 97 | $request->all(), 98 | $request->route()->parameters() 99 | ); 100 | 101 | if ($this->isCamelToSnake) { 102 | $inputs = UtilityService::fromCamelToSnake($inputs); 103 | if (isset($inputs['date_filter_column'])) { 104 | // i.e. if custom date_filter_column is passed from frontend, then transform it 105 | $inputs['date_filter_column'] = Str::snake($inputs['date_filter_column']); 106 | } 107 | } 108 | 109 | $result = $this->repo->{$this->indexCall}(["with" => $this->indexWith, "inputs" => $inputs]); 110 | 111 | return $this->standardResponse($result); 112 | } 113 | 114 | /** 115 | * @param $data 116 | * @param null $message 117 | * @param int $httpCode 118 | * @param null $type 119 | * @return \Illuminate\Http\JsonResponse 120 | */ 121 | public function standardResponse($data, $message = null, $httpCode = 200, $type = null) 122 | { 123 | if ($httpCode == 200 && $data && $this->isSnakeToCamel && (is_array($data) || is_object($data))) { 124 | $data = UtilityService::fromSnakeToCamel(json_decode(json_encode($data), true)); 125 | } 126 | return response()->json([ 127 | "message" => $message, 128 | "data" => $data && ($data instanceof Collection) ? $data->toArray() : $data, 129 | "type" => $type 130 | ], $httpCode); 131 | } 132 | 133 | /** 134 | * global show method , return selected $id row from Specific Model 135 | * @param Request $request 136 | * @param $id 137 | * @return \Illuminate\Http\JsonResponse 138 | * @throws \Illuminate\Contracts\Container\BindingResolutionException 139 | */ 140 | public function show(Request $request, $id) 141 | { 142 | if ($this->showRequest && $response = $this->validateRequest($this->showRequest)) return $response; 143 | 144 | $result = $this->repo->{$this->showCall}($id, ["with" => $this->showWith]); 145 | if ($this->transformer) { 146 | $transformer = app()->make($this->transformer); 147 | $result = $transformer->transform($result); 148 | } 149 | 150 | return $this->standardResponse($result); 151 | } 152 | 153 | /** 154 | * @param $method 155 | * @return bool|\Illuminate\Http\JsonResponse 156 | */ 157 | protected function validateRequest($method) 158 | { 159 | /** 160 | * create request object 161 | */ 162 | $this->request = app($method); 163 | $validator = $this->request->getValidator(); 164 | /** 165 | * check request is valid ? 166 | */ 167 | if ($validator->fails()) { 168 | return $this->standardResponse(null, $validator->errors()->messages(), 400, ErrorConstants::TYPE_VALIDATION_ERROR); 169 | } 170 | 171 | return false; 172 | } 173 | 174 | /** 175 | * @param Request $request 176 | * @return bool|\Illuminate\Http\JsonResponse 177 | */ 178 | public function store(Request $request) 179 | { 180 | $this->defaultMessage = $this->resourceName ? $this->resourceName . " created successfully" : "Resource Created successfully"; 181 | $this->jobMethod = $this->storeJobMethod ?? 'create'; 182 | if (!$this->createJob) throw new MethodNotAllowedHttpException(['message' => 'Method Not Allowed', 'code' => 405]); 183 | 184 | $data = array_replace_recursive( 185 | $request->json()->all(), 186 | $request->route()->parameters() 187 | ); 188 | 189 | if ($this->storeRequest && $response = $this->validateRequest($this->storeRequest)) return $response; 190 | 191 | if ($this->isCamelToSnake) { 192 | $data = UtilityService::fromCamelToSnake($data); 193 | } 194 | 195 | $data = $this->requestSanitizer($data, 'createExcept'); 196 | 197 | return $this->executeJob($request, $this->createJob, [ 198 | 'data' => $data, 199 | ]); 200 | } 201 | 202 | /** 203 | * @param $data 204 | * @param $modelKey 205 | * @return mixed 206 | */ 207 | private function requestSanitizer($data, $modelKey) 208 | { 209 | // removes keys which cannot be updated as defined in model class variable 210 | $exceptionKeys = []; 211 | if ($this->model) { 212 | $_model = new $this->model; 213 | $exceptionKeys = property_exists($_model, $modelKey) ? ($_model)->{$modelKey} : []; 214 | } 215 | 216 | if (count((array)$exceptionKeys)) { 217 | foreach ($exceptionKeys as $key) { 218 | if (array_key_exists($key, $data)) unset($data[$key]); 219 | } 220 | } 221 | 222 | return $data; 223 | } 224 | 225 | /** 226 | * @param $request 227 | * @param $jobClass 228 | * @param $params 229 | * @return \Illuminate\Http\JsonResponse 230 | */ 231 | protected function executeJob($request, $jobClass, $params) 232 | { 233 | $job = new $jobClass($params); 234 | 235 | if ($jobClass === BaseJob::class) { 236 | $job->method = $this->jobMethod; 237 | $job->event = $this->jobEvent; 238 | $job->repository = $this->jobRepository ? $this->jobRepository : $this->repository; 239 | } 240 | 241 | return $this->dispatchJob($request, $job, $params); 242 | 243 | } 244 | 245 | /** 246 | * @param $request 247 | * @param $job 248 | * @param $params 249 | * @return \Illuminate\Http\JsonResponse 250 | */ 251 | protected function dispatchJob($request, $job, $params) 252 | { 253 | $result = $this->dispatchSync($job); 254 | return $this->standardResponse($result, $this->customMessage ? $this->customMessage : $this->defaultMessage); 255 | } 256 | 257 | public function __call($method, $arguments) 258 | { 259 | parent::__call($method, $arguments); 260 | } 261 | 262 | /** 263 | * @param Request $request 264 | * @param $id 265 | * @return bool|\Illuminate\Http\JsonResponse 266 | */ 267 | public function update(Request $request, $id) 268 | { 269 | 270 | $this->defaultMessage = $this->resourceName ? $this->resourceName . " updated successfully" : "Resource Updated successfully"; 271 | $this->jobMethod = $this->updateJobMethod ?? 'update'; 272 | if (!$this->updateJob) throw new MethodNotAllowedHttpException(['message' => 'Method Not Allowed', 'code' => 405]); 273 | 274 | $data = array_replace_recursive( 275 | $request->json()->all(), 276 | $request->route()->parameters() 277 | ); 278 | if ($this->updateRequest && $response = $this->validateRequest($this->updateRequest)) return $response; 279 | 280 | if ($this->isCamelToSnake) { 281 | $data = UtilityService::fromCamelToSnake($data); 282 | } 283 | 284 | $data = $this->requestSanitizer($data, 'updateExcept'); 285 | 286 | 287 | return $this->executeJob($request, $this->updateJob, [ 288 | 'data' => $data, 289 | 'id' => $id 290 | ]); 291 | } 292 | 293 | /** 294 | * @param Request $request 295 | * @param $id 296 | * @return bool|\Illuminate\Http\JsonResponse 297 | * @throws MethodNotAllowedHttpException 298 | */ 299 | public function destroy(Request $request, $id) 300 | { 301 | $this->defaultMessage = $this->resourceName ? $this->resourceName . " deleted successfully" : "Resource deleted successfully"; 302 | $this->jobMethod = $this->deleteJobMethod ?? 'delete'; 303 | if (!$this->deleteJob) throw new MethodNotAllowedHttpException(['message' => 'Method Not Allowed', 'code' => 405]); 304 | 305 | if ($this->deleteRequest && $response = $this->validateRequest($this->deleteRequest)) return $response; 306 | 307 | return $this->executeJob($request, $this->deleteJob, [ 308 | 'id' => $id, 309 | 'data' => [] 310 | ]); 311 | } 312 | 313 | /** 314 | * @return \Illuminate\Contracts\Auth\Authenticatable|\Illuminate\Http\JsonResponse|null 315 | */ 316 | public function getUserByToken() 317 | { 318 | 319 | $user = Auth::user(); 320 | if (!$user) { 321 | return $this->standardResponse(null, "Invalid token. No user found.", 401, ErrorConstants::TYPE_AUTHORIZATION_ERROR); 322 | } 323 | return $user; 324 | } 325 | 326 | /** 327 | * Method which can be used as substitute for the Custom POST/PUT routes other than the resource route 328 | * @param $job 329 | * @param Request|null $request 330 | * @param array|null $additionalData 331 | * @return bool|\Illuminate\Http\JsonResponse 332 | */ 333 | public function handleCustomEndPoint($job, $request = null, $additionalData = null) 334 | { 335 | if ($this->customRequest && $response = $this->validateRequest($this->customRequest)) return $response; 336 | $data = []; 337 | if ($request) { 338 | $requestData = array_replace_recursive( 339 | $request->json()->all(), 340 | $request->route()->parameters() 341 | ); 342 | $data = UtilityService::fromCamelToSnake($requestData); 343 | } 344 | return $this->executeJob($request, $job, [ 345 | 'data' => $data, 346 | 'additionalData' => $additionalData 347 | ]); 348 | } 349 | 350 | /** 351 | * Method which can be used as substitute for the Custom GET(index) route other than the resource route 352 | * @param $job 353 | * @param Request|null $request 354 | * @param array $additionalData 355 | * @return \Illuminate\Http\JsonResponse 356 | */ 357 | public function handleCustomEndPointGet($job, $request, $additionalData = []) 358 | { 359 | $this->defaultMessage = null; 360 | if ($this->customRequest && $response = $this->validateRequest($this->customRequest)) return $response; 361 | $inputs = []; 362 | if ($request) { 363 | $inputs = array_replace_recursive( 364 | $request->all(), 365 | $request->route()->parameters() 366 | ); 367 | } 368 | 369 | if ($this->isCamelToSnake) { 370 | $inputs = UtilityService::fromCamelToSnake($inputs); 371 | } 372 | return $this->executeJob($request, $job, ["inputs" => $inputs, "additionalData" => $additionalData]); 373 | } 374 | 375 | /** 376 | * * Method which can be used as substitute for the Custom GET(show) route other than the resource route 377 | * @param $job 378 | * @param $request 379 | * @param $column 380 | * @param $value 381 | * @return \Illuminate\Http\JsonResponse 382 | */ 383 | public function handleCustomEndPointShow($job, $request, $column, $value) 384 | { 385 | $this->defaultMessage = null; 386 | if ($this->customRequest && $response = $this->validateRequest($this->customRequest)) return $response; 387 | 388 | $params = [ 389 | 'inputs' => [ 390 | $column => $value 391 | ] 392 | ]; 393 | if ($request) { 394 | $params['inputs'] = array_merge($params['inputs'], $request->all()); 395 | } 396 | 397 | return $this->executeJob($request, $job, $params); 398 | } 399 | 400 | /** 401 | * @return int|null 402 | */ 403 | protected function getLoggedInUserId() 404 | { 405 | return EnvironmentService::getLoggedInUserId(); 406 | } 407 | 408 | /** 409 | * @return null|object 410 | */ 411 | protected function getLoggedInUser() 412 | { 413 | return EnvironmentService::getLoggedInUser(); 414 | } 415 | 416 | protected function notImplemented($data, $message = null) 417 | { 418 | return $this->standardResponse($data, $message); 419 | } 420 | } 421 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Laravel Core Package 2 | Luezoid came up with a compact way to help one creating the APIs very fast & without much hassle. Using this package, one case easily create simple CRUD operations in Laravel framework in couple of minutes with just creating a few files & configuring the components. With a lot of efforts and deep analysis of day-to-day problems (we) developers faced in the past, we came up with a sub-framework of it's own kind to simplify & fasten up the REST APIs building process. 3 | 4 | A few cool features of this package are: 5 | 1. Simplest & fastest way to create [CRUD](#1-creating-crud)s. 6 | 2. Pre-built support to define table columns which are to be [specifically excluded](#2-exclude-columns-for-default-post--put-requests) before creating/updating a record(in default CRUD). 7 | 3. Pre-built [Search & Filters](#3-searching--filters) ready to use with just configuring components. 8 | 4. Pre-built [Pagination & Ordering](#4-pagination--ordering) of records ready in a **Standard communication format**. 9 | 5. [Relationship's data](#5-relationships-data) in the APIs(GET) is just a config thing. 10 | 6. Better way to correctly [fire an event](#6-attach-event-on-an-action-success) upon successful completion of an action. 11 | 7. [File uploads](#7-file-upload) has never been so easy before. Upload to local filesystem or AWS S3 bucket on the go. 12 | 8. Pre-built feature rich Service classes eg. [EnvironmentService](src/services/EnvironmentService.php), [RequestService](src/services/RequestService.php), [UtilityService](src/services/UtilityService.php), etc. 13 | 9. Nested Related models can be queried with simple config based approach from the code components. 14 | 10. [On the go `SELECT` & Relation filters can be passed with JSON & object operator(`->`) in query params](#10--select--relation-filters---select--where-query-over-table-columns--nested-relations) to select particular columns from a table(and related objects defined in models) making the API's response with less garbage data instead of writing custom query every time a new endpoint is created. 15 | 11. [Generic/Open Search](#11-genericopen-search) over a set of related objects(tables) with simple Array based config. 16 | >**Note:** For a complete working example of all these feature with core package pre-configured is available on this repository [luezoidtechnologies/laravel-core-base-repo-example](https://github.com/luezoidtechnologies/laravel-core-base-repo-example "laravel-core-base-repo-example"). 17 | 18 | ## Installation 19 | We recommend using [Composer](https://getcomposer.org) to install this package. This package supports [Laravel](https://laravel.com) versions >= 5.x. 20 | 21 | composer require "luezoid/laravel-core" # For latest version of Laravel (>=10.x) 22 | composer require "luezoid/laravel-core:^9.0" # For Laravel version 9.x 23 | composer require "luezoid/laravel-core:^8.0" # For Laravel version 8.x 24 | composer require "luezoid/laravel-core:^7.0" # For Laravel version 7.x 25 | composer require "luezoid/laravel-core:^6.0" # For Laravel version 6.x 26 | composer require "luezoid/laravel-core:^5.0" # For Laravel version 5.x 27 | Next, configure your `app/Exceptions/Handler.php` and extend it with `Luezoid\Laravelcore\Exceptions\Handler`. Sample file can be seen [here](/examples/Exceptions/Handler.php). 28 | 29 | ## 1. Creating CRUD 30 | Using this packages adds an extra entity between the Controller & Model paradigm in a normal MVC architecture. This extra entity we are referring here is called **Repository**. A **Repository** is a complete class where your whole business logic resides from getting the processed data from a **Controller** and saving it into database using **Model**(s). 31 | By using **Repository** as an intermediate between **Controller** & **Model**, we aim at maintaing clean code at **Controller's** end and making it a mediator which only receives data(from View, typically a REST route), validate it against the defined validation rules(if any, we uses **Request** class to define such rules), pre-process it(for eg. transform ***camelCased*** data from front-end into ***snake_case***) & sending business cooked data back the View. 32 | 33 | Let's start with creating a simple **Minions** CRUD. 34 | 35 | We have sample [migration](examples/migrations/2020_04_24_175321_create_minions_table.php) for table `minions`, model [`Minion`](/examples/Models/Minion.php), controller [`MinionController`](/examples/Controllers/MinionController.php) and repository [`MinionRepository`](/examples/Repositories/MinionRepository.php). 36 | Add these files into your application & adjust the namespaces accordingly. Then create a Route resource as below in [`routes/api.php`](examples/routes/api.php) and we are all ready: 37 | 38 | Route::resource('minions', 'MinionController', ['parameters' => ['minions' => 'id']]); 39 | Assuming your local server is running on port: 7872, try hitting REST endpoints as below: 40 | 41 | 1. POST /minions 42 | 43 | curl -X POST \ 44 | http://localhost:7872/api/minions \ 45 | -H 'Content-Type: application/json' \ 46 | -H 'cache-control: no-cache' \ 47 | -d '{ 48 | "name": "Stuart", 49 | "totalEyes": 2, 50 | "favouriteSound": "Grrrrrrrrrrr", 51 | "hasHairs": true 52 | }' 53 | 2. PUT /minions/1 54 | 55 | curl -X PUT \ 56 | http://localhost:7872/api/minions/1 \ 57 | -H 'Content-Type: application/json' \ 58 | -H 'cache-control: no-cache' \ 59 | -d '{ 60 | "name": "Stuart - The Pfff", 61 | "totalEyes": 2, 62 | "favouriteSound": "Grrrrrrrrrrr Pffffff", 63 | "hasHairs": false 64 | }' 65 | 3. DELETE /minions/1 66 | 67 | curl -X DELETE \ 68 | http://localhost:7872/api/minions/1 \ 69 | -H 'cache-control: no-cache' 70 | 4. GET /minions 71 | 72 | curl -X GET \ 73 | http://localhost:7872/api/minions \ 74 | -H 'cache-control: no-cache' 75 | 5. GET /minions/2 76 | 77 | curl -X GET \ 78 | http://localhost:7872/api/minions/2 \ 79 | -H 'cache-control: no-cache' 80 | 81 | ## 2. Exclude columns for default POST & PUT request(s) 82 | Refer the [`Minon`](examples/Models/Minion.php "Minon") model. We have the below public properties which can be used to define an array containing list of the columns to be specifically excluded for default **POST** & **PUT** requests of [CRUD](#1-creating-crud "CRUD"): 83 | 84 | // To exclude the key(s) if present in request body for default POST CRUD routes eg. POST /minions 85 | public $createExcept = [ 86 | 'id' 87 | ]; 88 | 89 | // To exclude the key(s) if present in request body for default PUT CRUD routes eg. PUT /minions/1 90 | public $updateExcept = [ 91 | 'total_eyes', 92 | 'has_hairs' 93 | ]; 94 | The major advantage for using such config in the model in the first place is: to provide a clean & elegant way & to simply reduce the coding efforts to be done just before saving the whole data into table. Typical examples could be: 95 | 1. You don't want to save a column value say ***is_email_verified*** if an attacker sends it the request body of POST /users; just add it into `$createExcept`. You need not to exclude this specifically in the codes/or request rules. 96 | 2. You don't want to update a column say ***username*** if an attacker sends it the request body of PUT /users/{id}; just add it into `$updateExcept`. You need not to exclude this specifically in the codes/or request rules. 97 | 98 | ## 3. Searching & Filters 99 | You can simply search over the list of available column(s) in the table for all GET requests. Let's begin with examples: 100 | - **General Searching** 101 | 102 | By default all the available columns in the tables are ready to be queried over GET request just by passing the key(s)-value(s) pair(s) in the query params. But to specifically mention it in the Model itsef, just define a public property `$searchable` which is an array containing the columns allowed to be searched. 103 | Let's say you want to search for all minions whose favourite sound is ***Pchhhh***. 104 | 105 | curl -X GET \ 106 | 'http://localhost:7872/api/minions?favouriteSound=Pchhhh' \ 107 | -H 'cache-control: no-cache' 108 | Response should contain all minions having ***Pchhhh*** string available in the column `favourite_sound` in the table `minions`. 109 | > Searching is using `LIKE` operator as `'%-SEARCH-STRING%'`. 110 | - **General Filtering** 111 | 112 | Similar to searching, you need to define public property `$filterable` which is again an array containing the columns allowed to be filtered. 113 | 114 | public $filterable = [ 115 | 'id', 116 | 'total_eyes', 117 | 'has_hairs' 118 | ]; 119 | Exact match will performed against these columns if present in the query params. 120 | 121 | Example: To find all the minions having `totalEyes` equal to 1: 122 | 123 | curl -X GET \ 124 | 'http://localhost:7872/api/minions?totalEyes=1' \ 125 | -H 'cache-control: no-cache' 126 | > Filtering is using `=` operator as `'total_eyes'=1`. 127 | - **Date Filters** 128 | 129 | Add the column which you want to be used for date filtering into `$filterable` property in the model class. Once done, you can now simply pass the query params **from** (AND/OR) **to** with the date(or datetime) values in standard MySQL format(`Y-m-d H:i:s`). 130 | Example: We want to search for all the minions which are created after **2020-04-25 09:25:20**: 131 | 132 | curl -X GET \ 133 | 'http://localhost:7872/api/minions?createdAt=2020-04-25%20%2009:25:20' \ 134 | -H 'cache-control: no-cache' 135 | You can also pass the column name in the query params over which you want the date search to be applied for. Just pass the key **dateFilterColumn** with the column name you want to use date search on (but note that the **$filterable** property must have this column specified in order to make things work). 136 | > **Notes:** 137 | >1. You may specify multiple key-value pairs in the query params & all the conditions will be queried with `AND` operators. 138 | >2. Pass all the variables in **camelCasing** & all will be transferred into **snake_casing** internally. You may configure this transformation **turning off** by **overriding properties** `$isCamelToSnake` & `$isSnakeToCamel` and setting them to `false` in [ApiCotroller](src/Http/Controllers/ApiController.php "ApiCotroller"). 139 | 140 | ## 4. Pagination & Ordering 141 | Did you notice the response of GET endpoint we just created above? Let's take a look in brief. Refer the [response](examples/Responses/get-minions-paginated-response.json "response") of **GET /minions** API. 142 | 143 | { 144 | "message": null, 145 | "data": { 146 | "items": [ 147 | {}, 148 | {}, 149 | ... 150 | ], 151 | "page": 1, // tells us about the current page 152 | "total": 6, // tells us the total results available (matching all the query params for searching/filtering applied) 153 | "pages": 1, // total pages in which the whole result set is distributed 154 | "perpage": 15 // total results per page 155 | }, 156 | "type": null 157 | } 158 | Pretty self explanatory, eh? 159 | 160 | You can pass query param `perpage=5` (to limit the per page size). Similarly, the `page=2` will grab the results of page 2. 161 | 162 | For ordering the result set by a particular column, just send query param key `orderby` with the name of column & a separate key `order` with value `ASC` for ascending (or) `DESC` for sorting in descending order. By default, results are sorted in descending order. 163 | 164 | Paginating & Ordering the results has never been so easy before :) 165 | > **Note:** Any GET(index) route retrieving results from a Repository (eg.[MinionRepository](src/examples/Repositories/MinionRepository.php "MinionRepository")) extending `\Luezoid\Laravelcore\Repositories\EloquentBaseRepository::getAll()` is all ready with such pagination & ordering. Make sure to use this pre-built feature & save time for manually implementing these features for every endpoint & grab a pint of beer to chill. 166 | 167 | ## 5. Relationship's data 168 | Let's assume each **Minion** leads a mission operated by **Gru** i.e. there is one-to-one relationship exists between Minion & Missions. See the `missions` table migrations([1](examples/migrations/2020_04_25_193714_create_missions_table.php),[2](examples/migrations/2020_04_25_193715_add_foreign_keys_to_missions_table.php)) and Model [`Mission`](examples/Models/Mission.php). To retrieve the leading mission by each Minion in GET requests(index & show), just add the relationship name in the [`MinionController`](/examples/Controllers/MinionController.php) properties as follows: 169 | - GET /minions 170 | 171 | protected $indexWith = [ 172 | 'leading_mission' // name of the hasOne() relationship defined in the Minion model 173 | ]; 174 | - GET /minions/2 175 | 176 | protected $showWith = [ 177 | 'leading_mission' // name of the hasOne() relationship defined in the Minion model 178 | ]; 179 | 180 | That's it. Just a config thingy & you can see in the response each **Minion** object contains another object **leadingMission** which is an instance of [`Mission`](examples/Models/Mission.php) model lead by respective Minion. 181 | 182 | > **Note:** For nested relationships, you can define them appending dot(.) operator eg. `employee.designations`. 183 | 184 | ## 6. Attach Event on an action success 185 | Let's arrange an [Event](#) to get triggered to bring a **Minion** to **Gru's** lab whenever a new [`Mission`](examples/Models/Mission.php) is created leading by the **Minion**. Create a POST route in [`routes/api.php`](examples/routes/api.php): 186 | 187 | Route::post('missions', 'MissionController@createMission')->name('missions.store'); 188 | 189 | We need to have [`MissionController`](examples/Controllers/MissionController.php), [`MissionRepository`](examples/Repositories/MissionRepository.php), [`MissionCreateRequest`](examples/Requests/MissionCreateRequest.php) and [`MissionCreateJob`](examples/Jobs/MissionCreateJob.php) ready for this route to work. 190 | Also we need to have an Event say [`BringMinionToLabEvent`](examples/Events/BringMinionToLabEvent.php) ready to be triggered & the same to be configured into job [`MissionCreateJob`](examples/Jobs/MissionCreateJob.php) under property `public $event = BringMinionToLabEvent::class;` 191 | Now try hitting the route POST /missions as follows: 192 | 193 | curl -X POST \ 194 | http://localhost:7872/api/missions \ 195 | -H 'Content-Type: application/json' \ 196 | -H 'cache-control: no-cache' \ 197 | -d '{ 198 | "name": "Steal the Moon! Part - 4", 199 | "description": "The first moon landing happened in 1969. Felonius Gru watched this historic moment with his mother and was inspired by the landing to go to outer space just like his idol Neil Armstrong.", 200 | "minionId": 2 201 | }' 202 | You should be able to see a log entry under file `storage/logs/laravel.log` which is the action we had set in the event [`BringMinionToLabEvent`](examples/Events/BringMinionToLabEvent.php). 203 | ## 7. File Upload 204 | With this package, file upload is just a config thing away. 205 | Publish the [`file.php`](src/config/file.php) configuration file to the config directory with below command: 206 | 207 | php artisan vendor:publish --tag=luezoid-file-config 208 | Configure the new `type` representing a specific module eg. **MINION_PROFILE_PICTURE** as per your requirement, define the `validation`(if any), `valid_file_types` allowed, `local_path`(for local filesystem), etc. A sample `type` named **EXAMPLE** is added by default for reference. 209 | Next, add the below code in `AppServiceProvide`: 210 | 211 | $this->app->bind(\Luezoid\Laravelcore\Contracts\IFile::class, function ($app) { 212 | if (config('file.is_local')) { 213 | return $app->make(\Luezoid\Laravelcore\Files\Services\LocalFileUploadService::class); 214 | } 215 | return $app->make(\Luezoid\Laravelcore\Files\Services\SaveFileToS3Service::class); 216 | }); 217 | Next, create a route as below: 218 | 219 | Route::post('files', '\Luezoid\Laravelcore\Http\Controllers\FileController@store')->name('files.store'); 220 | Now, you are all set to upload files. 221 | 222 | curl -X POST \ 223 | http://localhost:7872/api/files \ 224 | -H 'cache-control: no-cache' \ 225 | -H 'content-type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' \ 226 | -F type=EXAMPLE \ 227 | -F file=@/home/choxx/Desktop/a61113f5afc5ad52eb59f98ce293c266.jpg 228 | The response body will have an `id`, `url`, and a few other fields representing the storage location & other details for the uploaded file. See sample [here](examples/Responses/file-upload-response.json). This `id` field you could use to store in your table as foreign key column & make `hasOne()` relation with the records & use them as per need. 229 | 230 | Moreover, in the config itself you could configure if local filesystem has to be used for uploads or on the go AWS S3 bucket. For using S3 bucket, you need to configure AWS credentials in `.env` file: 231 | 232 | AWS_ACCESS_KEY_ID= 233 | AWS_SECRET_ACCESS_KEY= 234 | AWS_DEFAULT_REGION= 235 | Isn't is awesome? :) 236 | 237 | ## 10. Select & Relation Filters - `SELECT` & `WHERE` query over table columns (& nested relations) 238 | This is one of the coolest feature of this package. Tired off writing query to shorten up the result set & applying filters over nested relation data? Apply such filters with simplicity with just sending simple query params. Let's see how: 239 | 240 | - **Select Filters:** 241 | 242 | In the query params, a key-value pair can be passed with which you could simply restrict the number of columns to be present in the response for a particular model object & it's related model entities. The key to be sent is `selectFilters` & the value has to be minified JSON as explained below: 243 | 244 | **k** is keys, **r** is relation, **cOnly** is flag to set if only count is needed or the whole relational data. 245 | **cOnly** flag can be used in **r** relations nestedly. 246 | 247 | { 248 | "cOnly": false, 249 | "k": ["id", "name", "favouriteSound"], 250 | "r": { 251 | "missions": { 252 | "k": ["name", "description"] 253 | } 254 | } 255 | } 256 | So, with the above JSON, a GET request like: 257 | 258 | curl -X GET \ 259 | 'http://localhost:7872/api/minions?selectFilters={%22cOnly%22:false,%22k%22:[%22id%22,%22name%22,%22favouriteSound%22],%22r%22:{%22missions%22:{%22k%22:[%22name%22,%22description%22]}}}' \ 260 | -H 'cache-control: no-cache' 261 | would return only the columns `id`, `name` & `favourite_sound` of table `minions` and columns `name` & `description` of table `missions` in the [response](examples/Responses/json-filters-applied-on-listing-api.json). Rest redundant columns which are not needed are eleminated from the response causing substantial reduction in the size of overall response & speeding up the response time of APIs. 262 | > **Note:** both the columns the '**local key**' & the '**foreign key**' must be present in the main & the related relation(s). This whole config can go as deeper as needed in the same way as the first relation goes. 263 | 264 | - **Relation Filters:** 265 | 266 | Let's assume we want to find all the minions whose **Leading Mission** name is **"Steal the Moon!"**. Using `Eloquent` queries we can retrieve such results as: 267 | 268 | $query = Minion::whereHas('missions', function ($q) { 269 | $q->where('name', 'Steal the Moon!'); 270 | }); 271 | Seems easy right? But wait, for this to be dynamic, you need to customarily pass the query param holding this `missions.name` column & manage yourself by writing custom logic to make such filtering work. But by using this package, you can simply do it with just sending the query params as we have seen in [Searching & Filters](#3-searching--filters). You just need to send the query params as **relation-name->column-name={string-to-be-filtered}**. See the below example: 272 | 273 | curl -X GET \ 274 | 'http://localhost:7872/api/minions?missions-%3Ename=Steal%20the%20Moon%21' \ 275 | -H 'cache-control: no-cache' 276 | Notice that we have passed query param `missions-%3Ename=Steal%20the%20Moon%21` & hence reducing our effort of writing the above custom logic altogether. 277 | 278 | So, do we deserve a **star** rating? :) 279 | >**Note:** These Select & Relation filters can be combined together to reduce the response size as well as the size of bacck-end code. Make use of these two features combinely & make wonderful applications. 280 | 281 | ## 11. Generic/Open Search 282 | Need to send key `searchKey` via query params along with value. 283 | In repo, define config along with models & column names to be searched on. 284 | **Usage:** 285 | 286 | 287 | $searchConfig = [ 288 | 'abc' => [ 289 | 'model' => Abc::class, 290 | 'keys' => ['name', 'field'] 291 | ], 292 | 'groups' => [ 293 | 'model' => ProductGroup::class, 294 | 'keys' => ['name', 'code', 'hsn_code'] 295 | ] 296 | ]; 297 | return $this->search($searchConfig, $params); 298 | 299 | 300 | ## License 301 | Laravel-core is released under the MIT License. See the bundled [LICENSE](LICENSE) for details. 302 | -------------------------------------------------------------------------------- /src/Repositories/EloquentBaseRepository.php: -------------------------------------------------------------------------------- 1 | model())::insert($data['data']); 48 | 49 | if ($items) { 50 | return $data; 51 | } 52 | return null; 53 | } catch (Throwable $exception) { 54 | throw new AppException($exception->getMessage(), 500); 55 | } 56 | } 57 | 58 | /** 59 | * @param $data 60 | * @return mixed 61 | */ 62 | public function update($data) 63 | { 64 | $item = $this->find($data['id']); 65 | 66 | if (is_null($item)) { 67 | throw new NotFoundHttpException(); 68 | } 69 | 70 | $item->update($data["data"]); 71 | 72 | if (method_exists($this, 'afterUpdate')) 73 | 74 | $this->afterUpdate($item, $data); 75 | 76 | return $item; 77 | } 78 | 79 | /** 80 | * @param $id 81 | * @param null $params 82 | * @return mixed 83 | */ 84 | public function find($id, $params = null) 85 | { 86 | $query = null; 87 | $_model = new $this->model; 88 | $filterable = property_exists($_model, 'filterable') ? ($_model)->filterable : []; 89 | 90 | if (isset($params["with"]) && count($params["with"])) { 91 | $query = call_user_func_array([$this->model, 'with'], [$params['with']]); 92 | } 93 | 94 | if (isset($params["inputs"]) && $where = Arr::only((array)$params["inputs"], $filterable)) 95 | 96 | $query = call_user_func_array([$query ? $query : $this->model, 'where'], [$where]); 97 | 98 | return call_user_func_array([$query ? $query : $this->model, 'find'], isset($params['columns']) ? [$id, $params['columns']] : [$id]); 99 | } 100 | 101 | /** 102 | * @param $data 103 | * @return mixed 104 | */ 105 | public function delete($data) 106 | { 107 | $object = $this->find($data['id']); 108 | 109 | if (is_null($object)) { 110 | throw new NotFoundHttpException(); 111 | } 112 | 113 | $object->delete(); 114 | 115 | if (method_exists($this, 'afterDelete')) 116 | 117 | $this->afterDelete($object); 118 | 119 | return $object; 120 | 121 | } 122 | 123 | /** 124 | * @param $id 125 | * @param null $params 126 | * @return mixed 127 | */ 128 | public function show($id, $params = null) 129 | { 130 | $query = null; 131 | $_model = new $this->model; 132 | $filterable = property_exists($_model, 'filterable') ? ($_model)->filterable : []; 133 | 134 | if (isset($params["with"]) && count($params["with"])) { 135 | $query = call_user_func_array([$this->model, 'with'], [$params['with']]); 136 | } 137 | 138 | if (isset($params["inputs"]) && $where = Arr::only((array)$params["inputs"], $filterable)) 139 | 140 | $query = call_user_func_array([$query ? $query : $this->model, 'where'], [$where]); 141 | 142 | $data = call_user_func_array([$query ? $query : $this->model, 'find'], isset($params['columns']) ? [$id, $params['columns']] : [$id]); 143 | 144 | if (is_null($data)) { 145 | throw new NotFoundHttpException(); 146 | } 147 | 148 | return $data; 149 | } 150 | 151 | /** 152 | * @param $params 153 | * @param bool $first 154 | * @return mixed|null 155 | */ 156 | public function findByParamValue($params, $first = true) 157 | { 158 | $query = null; 159 | $_model = new $this->model; 160 | $tableName = property_exists($_model, 'table') ? ($_model)->table : null; 161 | $filterable = property_exists($_model, 'filterable') ? ($_model)->filterable : []; 162 | $_searchable = property_exists($_model, 'searchable') ? ($_model)->searchable : UtilityService::getColumnsForTable($this->model); 163 | $searchable = array_diff($_searchable, $filterable); 164 | 165 | if (isset($params["with"]) && count($params["with"])) { 166 | $query = call_user_func_array([$this->model, 'with'], [$params['with']]); 167 | } 168 | 169 | if ($where = Arr::only($params["inputs"], $searchable)) { 170 | foreach ($where as $param => $value) { 171 | if ($this->_checkInputValueType($value)) { 172 | if (is_array($value) || ($this->isJson($value) && ($value = json_decode($value, true)))) 173 | $query = call_user_func_array([$query ? $query : $this->model, 'whereIn'], [($tableName ? $tableName . '.' . $param : $param), $value]); 174 | else 175 | $query = call_user_func_array([$query ? $query : $this->model, 'where'], [($tableName ? $tableName . '.' . $param : $param), 'like', '%' . $value . '%']); 176 | } 177 | } 178 | } 179 | if ($where = Arr::only($params["inputs"], $filterable)) { 180 | $query = $this->addWhereToGetAll($query, $where, $tableName); 181 | } 182 | 183 | if ($first) { 184 | $query = call_user_func_array([$query, 'first'], []); 185 | } 186 | if (is_null($query)) { 187 | throw new NotFoundHttpException(); 188 | } 189 | return $query; 190 | } 191 | 192 | /** 193 | * @param $value 194 | * @return bool|int 195 | */ 196 | public function _checkInputValueType($value) 197 | { 198 | return is_string($value) ? strlen($value) : !empty($value); 199 | } 200 | 201 | /** 202 | * @param $str 203 | * @return bool 204 | */ 205 | public function isJson($str) 206 | { 207 | $json = json_decode($str); 208 | return $json && $str != $json; 209 | } 210 | 211 | /** 212 | * @param $query 213 | * @param $params 214 | * @param $tableName 215 | * @param null $model 216 | * @return mixed 217 | */ 218 | private function addWhereToGetAll($query, $params, $tableName, $model = null) 219 | { 220 | foreach ($params as $param => $value) { 221 | 222 | if ($this->_checkInputValueType($value)) { 223 | if (is_array($value) || ($this->isJson($value) && ($value = json_decode($value, true)))) 224 | $query = call_user_func_array([$query ? $query : $model ?? $this->model, 'whereIn'], [($tableName ? $tableName . '.' . $param : $param), $value]); 225 | else 226 | $query = call_user_func_array([$query ? $query : $model ?? $this->model, 'where'], [($tableName ? $tableName . '.' . $param : $param), $value]); 227 | } 228 | } 229 | return $query; 230 | } 231 | 232 | /** 233 | * @param $object 234 | * @return mixed 235 | */ 236 | public function findOrCreate($object) 237 | { 238 | $obj = $this->filter($object)->first(); 239 | 240 | if (!$obj) { 241 | $obj = $this->create(["data" => $object]); 242 | } 243 | 244 | 245 | return $obj; 246 | 247 | } 248 | 249 | /** 250 | * @param $data 251 | * @param bool $first 252 | * @param array $fields 253 | * @return mixed 254 | */ 255 | public function filter($data, $first = false, $fields = []) 256 | { 257 | return call_user_func_array([call_user_func_array([$this->model, 'where'], [$data]), $first ? 'first' : 'get'], $first || !$fields ? [] : [$fields]); 258 | } 259 | 260 | /** 261 | * @array $data 262 | * @param $data 263 | * @return mixed 264 | */ 265 | public function create($data) 266 | { 267 | $item = call_user_func_array([$this->model, 'create'], [$data["data"]]); 268 | if (method_exists($this, 'afterCreate')) { 269 | $this->afterCreate($item, $data); 270 | } 271 | 272 | return $item; 273 | } 274 | 275 | /** 276 | * @param $condition 277 | * @param $update 278 | * @param bool $in 279 | * @return mixed 280 | */ 281 | public function updateAll($condition, $update, $in = false) 282 | { 283 | return call_user_func_array([$this->model, 'where' . ($in ? 'In' : '')], $in ? $condition : [$condition])->update($update); 284 | } 285 | 286 | /** 287 | * @param $searchConfig 288 | * @param array $params 289 | * @return array 290 | * @throws BindingResolutionException 291 | */ 292 | public function search($searchConfig, $params = []) 293 | { 294 | $searchKey = $params['inputs']['search_key'] ?? null; 295 | $searchOn = $params['inputs']['search_on'] ? json_decode($params['inputs']['search_on']) : null; 296 | $selectFilters = Arr::get($params['inputs'], 'select_filters_mapping', '[]'); 297 | if ($this->isJson($selectFilters)) { 298 | $selectFilters = json_decode($selectFilters, true); 299 | } else { 300 | $selectFilters = []; 301 | } 302 | unset($params['inputs']['select_filters_mapping']); 303 | unset($params['inputs']['search_key']); 304 | unset($params['inputs']['search_on']); 305 | $result = []; 306 | foreach ($searchConfig as $key => $value) { 307 | if (!isset($searchOn) || array_search($key, $searchOn) > -1) { 308 | $query = null; 309 | $this->model = $value['model']; 310 | $searchableKeys = $value['keys']; 311 | if ($searchKey && !empty($searchableKeys)) { 312 | $query = $this->model::where($searchableKeys[0], 'like', '%' . $searchKey . '%'); 313 | for ($i = 1; $i < count($searchableKeys); $i++) { 314 | $query->orWhere($searchableKeys[$i], 'like', '%' . $searchKey . '%'); 315 | } 316 | } 317 | if (isset($selectFilters[$key])) { 318 | $params['inputs']['select_filters'] = json_encode(UtilityService::fromCamelToSnake($selectFilters[$key])); 319 | } 320 | $result[$key] = $this->getAll($params, $query); 321 | unset($params['inputs']['select_filters']); 322 | } 323 | } 324 | return $result; 325 | } 326 | 327 | /** 328 | * @param array $params 329 | * @param null $query 330 | * @return array 331 | * @throws BindingResolutionException 332 | */ 333 | public function getAll($params = [], $query = null) 334 | { 335 | $_model = new $this->model; 336 | $tableName = UtilityService::getModelTableName($this->model); 337 | $filterable = property_exists($_model, 'filterable') ? ($_model)->filterable : []; 338 | $_searchable = property_exists($_model, 'searchable') ? ($_model)->searchable : UtilityService::getColumnsForTable($this->model); 339 | $searchable = array_diff($_searchable, $filterable); 340 | $whereNullKeys = property_exists($_model, 'whereNullKeys') ? ($_model)->whereNullKeys : []; 341 | 342 | //use filter from inputs (?user_id=1&model=1389) 343 | if ($where = Arr::only($params["inputs"], $searchable)) { 344 | foreach ($where as $param => $value) { 345 | if ($this->_checkInputValueType($value)) { 346 | if (is_array($value) || ($this->isJson($value) && ($value = json_decode($value, true)))) { 347 | $query = call_user_func_array([$query ? $query : $this->model, 'whereIn'], [($tableName ? $tableName . '.' . $param : $param), $value]); 348 | } else 349 | $query = call_user_func_array([$query ? $query : $this->model, 'where'], [($tableName ? $tableName . '.' . $param : $param), 'like', '%' . $value . '%']); 350 | } 351 | } 352 | } 353 | 354 | if ($whereNullKeys = Arr::only($params["inputs"], $whereNullKeys)) { 355 | $query = $this->addWhereNullToGetAll($query, $whereNullKeys, $tableName); 356 | } 357 | if ($where = Arr::only($params["inputs"], $filterable)) { 358 | $query = $this->addWhereToGetAll($query, $where, $tableName); 359 | } 360 | 361 | 362 | // Added default query on date range if defined in $filterable property 363 | $dateFilterColumn = isset($params['inputs']['date_filter_column']) ? $params['inputs']['date_filter_column'] : 'created_at'; 364 | if (in_array($dateFilterColumn, $filterable)) { 365 | // if the date_filter_column is defined as filterable property, then only we search 366 | if (isset($params['inputs']['from'])) { 367 | $query = call_user_func_array([$query ?? $this->model, 'where'], [$tableName ? $tableName . '.' . $dateFilterColumn : $dateFilterColumn, '>=', $params['inputs']['from']]); 368 | } 369 | 370 | if (isset($params['inputs']['to'])) { 371 | $query = call_user_func_array([$query ?? $this->model, 'where'], [$tableName ? $tableName . '.' . $dateFilterColumn : $dateFilterColumn, '<=', $params['inputs']['to']]); 372 | } 373 | } 374 | 375 | //if need paginate must set page (?page=1&perpage=5) 376 | $page = intval(Arr::get($params['inputs'], 'page')); 377 | $perpage = Arr::get($params['inputs'], 'perpage'); 378 | 379 | //if set with 380 | if (isset($params["with"]) && count($params["with"])) { 381 | $query = call_user_func_array([$query ? $query : $this->model, 'with'], [$params['with']]); 382 | } 383 | 384 | if (in_array('id', $_searchable)) { 385 | //set order by from input or default ( id , desc ) 386 | $orderby = isset($params["inputs"]["orderby"]) ? $params["inputs"]["orderby"] : ($tableName ? $tableName . '.' . 'id' : 'id'); 387 | $order = isset($params["inputs"]["order"]) ? $params["inputs"]["order"] : "desc"; 388 | $query = call_user_func_array([$query ? $query : $this->model, 'orderby'], [$orderby, $order]); 389 | } 390 | $this->applyRelationAndSelectFilters($_model, $query, $params['inputs']); 391 | 392 | //if paginate set use paginate else return all result without paginate 393 | if ($page < 0) { 394 | 395 | $result = [ 396 | 'items' => call_user_func_array([$query ? $query : $this->model, 'get'], []) 397 | ]; 398 | } else { 399 | $result = $this->result_for_paginate(call_user_func_array([$query ? $query : $this->model, 'paginate'], [$perpage])); 400 | } 401 | 402 | return $result; 403 | 404 | } 405 | 406 | /** 407 | * @param $query 408 | * @param $keys 409 | * @param $tableName 410 | * @param null $model 411 | * @return mixed 412 | */ 413 | private function addWhereNullToGetAll($query, $keys, $tableName, $model = null) 414 | { 415 | 416 | foreach ($keys as $key => $value) { 417 | $query = call_user_func_array([$query ? $query : $model ?? $this->model, 'whereNull'], [($tableName ? $tableName . '.' . $key : $key)]); 418 | } 419 | return $query; 420 | } 421 | 422 | /** 423 | * @param $model 424 | * @param $query 425 | * @param $inputs 426 | */ 427 | protected function applyRelationAndSelectFilters($model, $query, $inputs) 428 | { 429 | $selectFilters = Arr::get($inputs, 'select_filters', '[]'); 430 | if ($this->isJson($selectFilters)) { 431 | $selectFilters = json_decode($selectFilters, true); 432 | $selectFilters = UtilityService::fromCamelToSnake($selectFilters); 433 | } else { 434 | $selectFilters = []; 435 | } 436 | unset($inputs['select_filters']); 437 | 438 | $relationFilters = []; 439 | if ($this->enableRelationFilters) { 440 | foreach ($inputs as $key => $value) { 441 | if (strpos($key, '->') == false) { 442 | continue; // not adding base keys 443 | } 444 | $keysArray = explode('->', $key); // exploding 445 | $lastRelation = &$relationFilters; // initially setting lastRelation to blank array as reference 446 | $lastRelationModel = $model; // TODO optimize this as common model is still getting validated again 447 | $keysCount = count($keysArray) - 1; 448 | foreach ($keysArray as $index => $relation) { 449 | if ($keysCount != $index) { 450 | if (!method_exists($lastRelationModel, $relation)) { 451 | throw new BadRequestHttpException("Invalid relation in query params: $relation"); 452 | continue; 453 | } 454 | $lastRelationModel = get_class((new $lastRelationModel)->{$relation}()->getRelated()); 455 | } 456 | if (!($lastRelation[$relation] ?? false)) { 457 | $lastRelation[$relation] = []; // adding relation if not exist 458 | } 459 | $lastRelation = &$lastRelation[$relation]; // moving pointer ahead to point & add to the last node 460 | } 461 | $lastRelation = $value; // at last setting the last node with the value provided in input 462 | unset($lastRelation); // unsetting the variable 463 | } 464 | $this->applyRelationFilters($query, $relationFilters, $this->model); 465 | } 466 | 467 | if ($this->enableSelectFilters) { 468 | // populating $selectFilters as per relation filters 469 | $this->populateWhereConditionsForSelectFilters($relationFilters, $selectFilters); 470 | 471 | // applying select filters 472 | $this->applySelectFilters($query, $selectFilters); 473 | } 474 | } 475 | 476 | /** 477 | * @param $query 478 | * @param $filters 479 | * @param $model 480 | */ 481 | private function applyRelationFilters(&$query, $filters, $model) 482 | { 483 | $_model = new $model; 484 | foreach ($filters as $relationKey => $value1) { 485 | if (is_array($value1)) { 486 | if ($this->selectFilterType == 'whereHas') { 487 | $query->{$this->selectFilterType}($relationKey, function ($q) use ($relationKey, $value1, $_model) { 488 | $relationKeyClass = get_class($_model->{$relationKey}()->getRelated()); 489 | $this->applyRelationFilters($q, $value1, $relationKeyClass); 490 | }); 491 | } else { 492 | $query->{$this->selectFilterType}([$relationKey => function ($q) use ($relationKey, $value1, $_model) { 493 | $relationKeyClass = get_class($_model->{$relationKey}()->getRelated()); 494 | $this->applyRelationFilters($q, $value1, $relationKeyClass); 495 | }]); 496 | } 497 | } else { 498 | $tableName = $_model->getTable(); 499 | $filterable = property_exists($_model, 'filterable') ? ($_model)->filterable : []; 500 | $_searchable = property_exists($_model, 'searchable') ? ($_model)->searchable : Schema::getColumnListing($tableName); 501 | $searchable = array_diff($_searchable, $filterable); 502 | $whereNullKeys = property_exists($_model, 'whereNullKeys') ? ($_model)->whereNullKeys : []; 503 | $filter = [$relationKey => $value1]; 504 | if ($where = Arr::only($filter, $searchable)) { 505 | foreach ($where as $param => $value2) { 506 | if ($this->_checkInputValueType($value2)) { 507 | if (is_array($value2) || ($this->isJson($value2) && ($value2 = json_decode($value2, true)))) { 508 | $query = call_user_func_array([$query ? $query : $model, 'whereIn'], [($tableName ? $tableName . '.' . $param : $param), $value2]); 509 | } else 510 | $query = call_user_func_array([$query ? $query : $model, 'where'], [($tableName ? $tableName . '.' . $param : $param), 'like', '%' . $value2 . '%']); 511 | } 512 | } 513 | } 514 | 515 | if ($whereNullKeys = Arr::only($filter, $whereNullKeys)) { 516 | $query = $this->addWhereNullToGetAll($query, $whereNullKeys, $tableName, $model); 517 | } 518 | if ($where = Arr::only($filter, $filterable)) { 519 | $query = $this->addWhereToGetAll($query, $where, $tableName, $model); 520 | } 521 | } 522 | } 523 | } 524 | 525 | /** 526 | * @param $relationFilters 527 | * @param $selectFilters 528 | */ 529 | protected function populateWhereConditionsForSelectFilters($relationFilters, &$selectFilters) 530 | { 531 | foreach ($relationFilters as $relationKey => $relationValue) { 532 | if (is_array($relationValue)) { 533 | if (!isset($selectFilters['k'])) { 534 | $selectFilters['k'] = []; 535 | } 536 | if (!isset($selectFilters['r'])) { 537 | $selectFilters['r'] = []; 538 | } 539 | if (!isset($selectFilters['c_only'])) { 540 | $selectFilters['c_only'] = false; 541 | } 542 | if (!isset($selectFilters['r'][$relationKey])) { 543 | $selectFilters['r'][$relationKey] = [ 544 | 'k' => [], 545 | 'r' => null, 546 | 'c' => null, 547 | 'c_only' => false 548 | ]; 549 | } 550 | $this->populateWhereConditionsForSelectFilters($relationValue, $selectFilters['r'][$relationKey]); 551 | } else { 552 | if (!isset($selectFilters['c'])) { 553 | $selectFilters['c'] = []; 554 | } 555 | $selectFilters['c'] = array_merge($selectFilters['c'], [$relationKey => $relationValue]); 556 | } 557 | } 558 | } 559 | 560 | /** 561 | * @param $query 562 | * @param $filters 563 | * @param null $relation 564 | */ 565 | protected function applySelectFilters(&$query, $filters, $relation = null) 566 | { 567 | if (is_array($filters['k'] ?? null)) { 568 | $select = []; 569 | foreach ($filters['k'] as $k) { 570 | array_push($select, Str::snake($k)); 571 | } 572 | if (count($select)) { 573 | $query->select($select); 574 | } 575 | } 576 | if ($filters['c'] ?? false) { 577 | $where = []; 578 | foreach ($filters['c'] as $key => $value) { 579 | if ($this->isJson($value) && ($value = json_decode($value, true))) { 580 | $query->whereIn($key, $value); 581 | } else { 582 | array_push($where, [$key, $value]); 583 | } 584 | } 585 | if (count($where)) { 586 | $query->where($where); 587 | } 588 | } 589 | if (!is_null($filters['r'] ?? null) && is_array($filters['r'] ?? [])) { 590 | foreach ($filters['r'] as $relation => $filter) { 591 | if ($filter['c_only'] ?? false) { 592 | $query->withCount($relation); 593 | } else { 594 | $query->with([ 595 | $relation => function ($q) use ($filter, $relation) { 596 | $this->applySelectFilters($q, $filter, $relation); 597 | } 598 | ]); 599 | } 600 | } 601 | } 602 | } 603 | 604 | /** 605 | * @param $collection 606 | * @return array 607 | * @throws BindingResolutionException 608 | */ 609 | public function result_for_paginate($collection) 610 | { 611 | return [ 612 | "items" => !empty($this->indexTransformer) ? app()->make($this->indexTransformer)->transformCollection($collection->items()) : $collection->items(), 613 | "page" => $collection->currentPage(), 614 | "total" => $collection->total(), 615 | "pages" => $collection->lastPage(), 616 | "perpage" => $collection->perPage() 617 | ]; 618 | } 619 | 620 | /** 621 | * @param $createConditionalParams 622 | * @param $updateParams 623 | * @param bool $withTrashed 624 | * @return mixed 625 | * @throws AppException 626 | */ 627 | public function createOrUpdate($createConditionalParams, $updateParams, $withTrashed = false) 628 | { 629 | try { 630 | DB::beginTransaction(); 631 | if ($withTrashed) { 632 | $this->model::where($createConditionalParams)->withTrashed()->restore(); 633 | } 634 | $result = $this->model::updateOrCreate($createConditionalParams, $updateParams); 635 | DB::commit(); 636 | return $result; 637 | } catch (Throwable $exception) { 638 | DB::rollBack(); 639 | throw new AppException($exception->getMessage()); 640 | } 641 | } 642 | } 643 | --------------------------------------------------------------------------------