├── 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 |
--------------------------------------------------------------------------------