├── .gitignore ├── src ├── Models │ ├── Dud.php │ ├── Model.php │ ├── WithSoftDeletes.php │ └── Traits │ │ ├── Notifiable.php │ │ ├── Helper.php │ │ └── Searchable.php ├── Notifications │ ├── SendOnQueue.php │ ├── DBChannel.php │ └── Send.php ├── Docs │ └── Scribe │ │ └── Strategies │ │ ├── EnhanceMetadata.php │ │ ├── UseTestResponses.php │ │ ├── Metadata │ │ └── EnhanceMetadata.php │ │ └── Responses │ │ └── UseTestResponses.php ├── Helpers │ ├── Blueprint.php │ ├── Config.php │ ├── DB.php │ ├── MailMessage.php │ └── Upload.php ├── config │ └── laraquick.php ├── Controllers │ └── Traits │ │ ├── Response │ │ ├── Web.php │ │ ├── Api.php │ │ └── Common.php │ │ ├── Crud │ │ ├── Authorize.php │ │ ├── Show.php │ │ ├── Crud.php │ │ ├── Validation.php │ │ ├── Update.php │ │ ├── Store.php │ │ ├── Index.php │ │ └── Destroy.php │ │ ├── Helpers │ │ ├── Pivotable.php │ │ ├── Referer.php │ │ └── Attachable.php │ │ ├── Web.php │ │ ├── Api.php │ │ └── PassThrough.php ├── Tests │ └── Traits │ │ ├── Http.php │ │ ├── Common.php │ │ └── Api.php ├── Jobs │ └── AsyncCall.php └── Providers │ └── ServiceProvider.php ├── docker-compose.yml ├── Dockerfile ├── LICENSE ├── composer.json ├── README.md └── CHANGELOG.md /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor -------------------------------------------------------------------------------- /src/Models/Dud.php: -------------------------------------------------------------------------------- 1 | toDatabase($notifiable); 12 | 13 | $data['id'] = $notification->id; 14 | $data['read_at'] = null; 15 | 16 | return $notifiable->routeNotificationFor('database')->create($data); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Helpers/Blueprint.php: -------------------------------------------------------------------------------- 1 | indexCommand('fullText', $columns, $name, $algorithm); 12 | } 13 | 14 | public function dropFullText($index) 15 | { 16 | return $this->dropIndexCommand('dropFullText', 'fullText', $index); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Models/Traits/Notifiable.php: -------------------------------------------------------------------------------- 1 | morphMany(config('laraquick.classes.database_notification', DatabaseNotification::class), 'notifiable')->orderBy('created_at', 'desc'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/config/laraquick.php: -------------------------------------------------------------------------------- 1 | [ 5 | // User model must use \Laraquick\Models\Traits\Notifiable 6 | 'database_notification' => \Illuminate\Notifications\DatabaseNotification::class 7 | ], 8 | 'controllers' => [ 9 | 'use_policies' => false 10 | ], 11 | 'tests' => [ 12 | // Headers to pass into every request 13 | 'headers' => [], 14 | 15 | 'responses' => [ 16 | 17 | // The path in the storage where responses are to be stored 18 | 'storage_path' => 'test-responses', 19 | 20 | // The file format for all stored test responses 21 | 'format' => 'json', 22 | 23 | ], 24 | ], 25 | ]; 26 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PHP_VERSION=${PHP_VERSION:-8.2} 2 | FROM php:${PHP_VERSION}-fpm-alpine AS php-system-setup 3 | 4 | # Install system dependencies 5 | RUN apk update && apk add dcron busybox-suid curl libcap zip unzip git php-pcntl 6 | 7 | # Install PHP extensions 8 | COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/bin/ 9 | RUN install-php-extensions zip pdo_pgsql pcntl 10 | RUN install-php-extensions gd 11 | 12 | # Install composer 13 | COPY --from=composer/composer:2 /usr/bin/composer /usr/local/bin/composer 14 | 15 | FROM php-system-setup AS app-setup 16 | 17 | # Set working directory 18 | ENV LARAVEL_PATH=/var/www/html 19 | WORKDIR $LARAVEL_PATH 20 | 21 | # Switch to non-root 'app' user & install app dependencies 22 | COPY composer.* ./ 23 | 24 | # Copy app 25 | COPY . $LARAVEL_PATH/ 26 | 27 | EXPOSE 80 28 | -------------------------------------------------------------------------------- /src/Controllers/Traits/Response/Web.php: -------------------------------------------------------------------------------- 1 | withStatus($status); 18 | } 19 | 20 | /** 21 | * Called when an error occurs while performing an action 22 | * 23 | * @param string $message 24 | * @param mixed $errors 25 | * @param integer $code 26 | * @return Response 27 | */ 28 | protected function error($message, $errors = null, $code = 400) 29 | { 30 | $back = back()->withInput()->withMessage($message); 31 | 32 | if ($errors) { 33 | $back->withErrors($errors); 34 | } 35 | 36 | return $back; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Ezra Obiwale 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/Controllers/Traits/Crud/Authorize.php: -------------------------------------------------------------------------------- 1 | usePolicy()) { 27 | return; 28 | } 29 | 30 | $map = $this->resourceAbilityMap(); 31 | 32 | if (array_key_exists($method, $map)) { 33 | $this->authorize($map[$method], $arguments); 34 | } 35 | } 36 | 37 | /** 38 | * Indicates whether to use policy for the crud methods 39 | */ 40 | protected function usePolicy(): bool 41 | { 42 | return config('laraquick.controllers.use_policies', false); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Tests/Traits/Http.php: -------------------------------------------------------------------------------- 1 | asUser($user); 18 | 19 | return parent::actingAs($user, $driver); 20 | } 21 | 22 | /** 23 | * Acts as a user when using request() with jwt headers 24 | * 25 | * @param Authenticatable $uiser 26 | * @return self 27 | */ 28 | protected function asUser(Authenticatable $user): self 29 | { 30 | $this->user = $user; 31 | 32 | return $this; 33 | } 34 | 35 | /** 36 | * Adds headers to the request 37 | * 38 | * @return self 39 | */ 40 | protected function addHeaders(): self 41 | { 42 | return $this->withHeaders($this->headers()); 43 | } 44 | 45 | /** 46 | * Return request headers needed to interact with the API. 47 | * 48 | * @return array Array of headers. 49 | */ 50 | protected function headers(): array 51 | { 52 | return array_merge(['Accept' => 'application/json'], Config::get('laraquick.tests.headers', [])); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "d-scribe/laraquick", 3 | "type": "library", 4 | "description": "A collection of classes to be extended/used in laravel applications for quick development", 5 | "license": "MIT", 6 | "keywords": [ 7 | "laravel", 8 | "quick", 9 | "api", 10 | "web", 11 | "controller", 12 | "traits" 13 | ], 14 | "authors": [ 15 | { 16 | "name": "Ezra Obiwale", 17 | "email": "contact@ezraobiwale.com", 18 | "homepage": "http://ezraobiwale.com", 19 | "role": "Developer" 20 | } 21 | ], 22 | "require": { 23 | "php": ">=8.2", 24 | "guzzlehttp/guzzle": ">=7.7", 25 | "predis/predis": "^2.2", 26 | "spatie/laravel-query-builder": "^5.0" 27 | }, 28 | "extra": { 29 | "laravel": { 30 | "providers": [ 31 | "Laraquick\\Providers\\ServiceProvider" 32 | ] 33 | } 34 | }, 35 | "autoload": { 36 | "psr-4": { 37 | "Laraquick\\": "src/" 38 | } 39 | }, 40 | "suggest": { 41 | "d-scribe/laravel-db-command": "A laravel command for database operations", 42 | "knuckleswtf/scribe": "Generate API documentation for humans" 43 | }, 44 | "minimum-stability": "dev", 45 | "prefer-stable": true 46 | } 47 | -------------------------------------------------------------------------------- /src/Helpers/Config.php: -------------------------------------------------------------------------------- 1 | 's3', 13 | 'key' => env('AWS_ACCESS_KEY_ID'), 14 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 15 | 'region' => env('AWS_DEFAULT_REGION'), 16 | 'bucket' => env('AWS_BUCKET'), 17 | 'endpoint' => env('AWS_ENDPOINT'), 18 | 'url' => env('AWS_URL'), 19 | 'use_path_style_endpoint' => true, 20 | 'bucket_endpoint' => false, 21 | 'options' => [ 22 | 'override_visibility_on_copy' => 'private', 23 | ], 24 | 'cache' => [ 25 | 'store' => 'redis', 26 | 'expire' => 600, 27 | 'prefix' => 's3-cache', 28 | ], 29 | 'throw' => false, 30 | ]; 31 | 32 | if ($root) { 33 | $defaults['root'] = self::joinPaths($prefix, $root); 34 | } 35 | 36 | return array_merge($defaults, $config); 37 | } 38 | 39 | private static function joinPaths(string $path1, string $path2): string 40 | { 41 | return preg_replace( 42 | '/\\' . DIRECTORY_SEPARATOR . '+/', 43 | DIRECTORY_SEPARATOR, 44 | $path1 . DIRECTORY_SEPARATOR . $path2 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Controllers/Traits/Response/Api.php: -------------------------------------------------------------------------------- 1 | is_array($response) && array_key_exists('status', $response) 22 | ? $response['status'] : 'ok' 23 | ]; 24 | 25 | if (!is_null($response)) { 26 | if (!is_array($response) && !is_object($response)) { 27 | $resp['message'] = $this->translate($response); 28 | } elseif ($response !== null) { 29 | $resp['data'] = $response; 30 | } 31 | } 32 | 33 | if (count($meta)) { 34 | $resp['meta'] = $meta; 35 | } 36 | 37 | return response()->json($resp, $code); 38 | } 39 | 40 | /** 41 | * Called when an error occurs while performing an action 42 | * 43 | * @param string $message 44 | * @param mixed $errors 45 | * @param integer $code 46 | * @return JsonResponse 47 | */ 48 | protected function error($message, $errors = null, $code = 400) 49 | { 50 | $resp = [ 51 | "status" => "error", 52 | "message" => $this->translate($message) 53 | ]; 54 | 55 | if ($errors !== null) { 56 | $resp["errors"] = $errors; 57 | } 58 | 59 | return response()->json($resp, $code); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Docs/Scribe/Strategies/Metadata/EnhanceMetadata.php: -------------------------------------------------------------------------------- 1 | controller->name)); 32 | $controller = Str::beforeLast($controller, 'Controller'); 33 | $controller = str_replace('-', ' ', Str::kebab($controller)); 34 | 35 | return [ 36 | 'description' => str_replace( 37 | ['items', 'item'], 38 | [Str::plural($controller), $controller], 39 | $endpointData->metadata->description 40 | ) 41 | ]; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/Helpers/DB.php: -------------------------------------------------------------------------------- 1 | tableName = $tableName; 15 | } 16 | 17 | public static function table($tableName): self 18 | { 19 | return new static($tableName); 20 | } 21 | 22 | public function fullText($columns): self 23 | { 24 | if (!is_array($columns)) { 25 | $columns = [$columns]; 26 | } 27 | 28 | $key = $this->getFullTextKeyName($columns); 29 | $columns_string = implode(',', $columns); 30 | 31 | iDB::statement("ALTER TABLE {$this->tableName} ADD FULLTEXT {$key} ({$columns_string})"); 32 | 33 | return $this; 34 | } 35 | 36 | protected function getFullTextKeyName(array $columns): string 37 | { 38 | return implode('_', $columns) . '_fulltext'; 39 | } 40 | 41 | public function dropFullText($columns): self 42 | { 43 | if (!is_array($columns)) { 44 | $columns = [$columns]; 45 | } 46 | 47 | $key = $this->getFullTextKeyName($columns); 48 | iDB::statement("ALTER TABLE {$this->tableName} DROP INDEX {$key}"); 49 | 50 | return $this; 51 | } 52 | 53 | public static function transaction(callable $func, callable $catch = null) 54 | { 55 | try { 56 | iDB::beginTransaction(); 57 | $result = call_user_func($func); 58 | iDB::commit(); 59 | 60 | return $result; 61 | } catch (Exception $ex) { 62 | iDB::rollback(); 63 | 64 | if ($catch) { 65 | return call_user_func($catch, $ex); 66 | } else { 67 | throw new Exception($ex->getMessage(), $ex->getCode(), $ex); 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Controllers/Traits/Helpers/Pivotable.php: -------------------------------------------------------------------------------- 1 | attached($id, $this->relation()); 44 | } 45 | 46 | /** 47 | * Attaches a list of items to the object at the given id 48 | * 49 | * @param int $id 50 | * @return mixed 51 | */ 52 | public function addItems($id) 53 | { 54 | return $this->attach($id, $this->relation(), $this->paramKey()); 55 | } 56 | 57 | /** 58 | * Detaches a list of items from the object at the given id 59 | * 60 | * @param int $id 61 | * @return mixed 62 | */ 63 | public function removeItems($id) 64 | { 65 | return $this->detach($id, $this->relation(), $this->paramKey()); 66 | } 67 | 68 | /** 69 | * Syncs a list of items with existing attached items on the object at the given id 70 | * 71 | * @param int $id 72 | * @return mixed 73 | */ 74 | public function updateItems($id) 75 | { 76 | return $this->sync($id, $this->relation(), $this->paramKey()); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Controllers/Traits/Helpers/Referer.php: -------------------------------------------------------------------------------- 1 | headers->get('referer'); 24 | $refOrigin = $this->originFromUrl($referer); 25 | 26 | foreach ($url as $urll) { 27 | if ($this->urlsMatch($refOrigin, $urll, $allowSubdomains)) { 28 | return; 29 | } 30 | } 31 | 32 | throw new \Exception($referer . ' is not a valid domain.'); 33 | } 34 | 35 | protected function urlsMatch($url1, $url2, $ignoreSubdomains = false): bool 36 | { 37 | $refOrigin = $this->originFromUrl($url1); 38 | $urlOrigin = $this->originFromUrl($url2); 39 | 40 | if ($refOrigin == $urlOrigin) { 41 | return true; 42 | } elseif ($ignoreSubdomains) { 43 | $refParts = explode('.', $refOrigin); 44 | array_shift($refParts); 45 | $urlParts = explode('.', $urlOrigin); 46 | array_shift($urlParts); 47 | 48 | if (join('.', $refParts) == join('.', $urlParts)) { 49 | return true; 50 | } 51 | } 52 | 53 | return false; 54 | } 55 | 56 | /** 57 | * Gets the origin from the url string 58 | * 59 | * @param string $url 60 | * @return string 61 | */ 62 | protected function originFromUrl($url): string 63 | { 64 | return Str::before(Str::after($url, '://'), '/'); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Tests/Traits/Common.php: -------------------------------------------------------------------------------- 1 | app->instance($className, $mocked); 28 | 29 | return $result; 30 | } 31 | 32 | /** 33 | * Save the response of a test to storage 34 | * 35 | * @param TestResponse $response 36 | * @param string $path 37 | * @param array $overrideWith An array of key=>values to override on the stored response 38 | * @return string 39 | */ 40 | protected function storeResponse(TestResponse $response, $path, $overrideWith = []): string 41 | { 42 | // document the response by creating a log file and streaming details to it. 43 | if (Str::endsWith($path, '.json')) { 44 | $path = Str::before($path, '.json'); 45 | } 46 | 47 | $path = str_replace('.', '/', $path) . '/' . $response->getStatusCode(); 48 | $storagePath = Config::get('laraquick.tests.responses.storage_path', 'test-responses'); 49 | $format = Config::get('laraquick.tests.responses.format', ''); 50 | 51 | if ($format) { 52 | $format = '.' . $format; 53 | } 54 | 55 | $data = $response->getStatusCode() === 204 ? '' : collect($response->json())->merge($overrideWith)->all(); 56 | 57 | return Storage::put("$storagePath/$path" . $format, json_encode($data, JSON_PRETTY_PRINT)); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # laraquick 2 | 3 | A collection of classes to be extended/used in laravel applications for quick 4 | development. 5 | 6 | ## Introduction 7 | 8 | The library contains traits with well documented methods that should be used by 9 | controllers and models to enhance coding speed. 10 | 11 | ## Installation 12 | 13 | ``` 14 | composer require d-scribe/laraquick 15 | ``` 16 | 17 | ## Dependencies 18 | 19 | ### >= v1.* 20 | 21 | - PHP >= 7.0 22 | - Laravel - ~5.5 23 | - Guzzle - ~6.0 24 | 25 | ### v0.* 26 | 27 | - PHP >= 5.6.0 28 | - Laravel - 5.4.* 29 | - Laravel Fractal - ^4.0 30 | - Guzzle - ~6.0 31 | 32 | ## Example 33 | 34 | An example controller for a `Book` model is: 35 | 36 | ```php 37 | use App\Book; 38 | use Laraquick\Controllers\Traits\Api; 39 | 40 | class BookController extends Controller { 41 | 42 | use Api; 43 | 44 | protected function model(): string 45 | { 46 | return Book::class; 47 | } 48 | 49 | // if you have a custom form request class 50 | protected function validationRequest(): string 51 | { 52 | return BookRequest::class; 53 | } 54 | 55 | // if you don't have a custom form request class 56 | protected function validationRules(array $data, $id = null): array 57 | { 58 | return [ 59 | 'title' => 'required|max:200', 60 | 'author' => 'required|max:50', 61 | 'genre' => 'required' 62 | ]; 63 | } 64 | } 65 | 66 | ``` 67 | 68 | And with just the above, the controller would take care of listing (w/ pagination), 69 | and all `CRUD` operations and give the right JSON responses. 70 | 71 | ```php 72 | Route::httpResource('books', BookController::class); 73 | ``` 74 | 75 | ### What if Web and not API? 76 | 77 | Oh, that's covered too with the version 1.5 and above. Just swap out the `Api` 78 | trait for its `Web` counterpart, and you're good. 79 | 80 | ## Documentation 81 | 82 | [Get a full walk-through](http://laraquick.readme.io) 83 | 84 | ## Contribution 85 | 86 | Contributions are absolutely welcome. Create a PR and I'll as swiftly as possible 87 | merge it up. 88 | -------------------------------------------------------------------------------- /src/Helpers/MailMessage.php: -------------------------------------------------------------------------------- 1 | table['title'] = $title; 14 | 15 | return $this; 16 | } 17 | 18 | public function setTableHeaders(array $headers) : self 19 | { 20 | $this->table['headers'] = $headers; 21 | 22 | return $this; 23 | } 24 | 25 | public function addTableRow(array $rowData, int $rowIndex = null) : self 26 | { 27 | if ($rowIndex) { 28 | $this->table['rows'][$rowIndex] = $rowData; 29 | } else { 30 | $this->table['rows'][] = $rowData; 31 | } 32 | 33 | return $this; 34 | } 35 | 36 | public function setTableFooters(array $footers) : self 37 | { 38 | $this->table['footers'] = $footers; 39 | 40 | return $this; 41 | } 42 | 43 | public function table(array $table) : self 44 | { 45 | $this->table = $table; 46 | 47 | return $this; 48 | } 49 | 50 | /** 51 | * @inheritDoc 52 | */ 53 | public function with($line) 54 | { 55 | if ($line instanceof Action) { 56 | $this->action($line->text, $line->url); 57 | } elseif (!$this->actionText && !count($this->table)) { 58 | $this->introLines[] = $this->formatLine($line); 59 | } else { 60 | $this->outroLines[] = $this->formatLine($line); 61 | } 62 | 63 | return $this; 64 | } 65 | 66 | /** 67 | * Creates the MailMessage from an array of data 68 | * 69 | * @param array $data 70 | * @return self 71 | */ 72 | public function hydrate(array $data) 73 | { 74 | foreach ($data as $property => $value) { 75 | if (property_exists($this, $property)) { 76 | $this->$property = $value; 77 | } 78 | } 79 | 80 | return $this; 81 | } 82 | 83 | public function toArray() : array 84 | { 85 | $array = parent::toArray(); 86 | $array['table'] = $this->table; 87 | return $array; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Controllers/Traits/Crud/Show.php: -------------------------------------------------------------------------------- 1 | showModel(); 50 | 51 | if (!$model) { 52 | return $this->modelNotSetError('Show model undefined'); 53 | } 54 | 55 | $item = $this->find($model, $id); 56 | 57 | if (!$item) { 58 | return $this->notFoundError(); 59 | } 60 | 61 | if (method_exists($this, 'authorizeMethod')) { 62 | $this->authorizeMethod('show', [$model, $item]); 63 | } 64 | 65 | if ($resp = $this->beforeShowResponse($item)) { 66 | return $resp; 67 | } 68 | 69 | return $this->showResponse($item); 70 | } 71 | 72 | /** 73 | * Called when the model is found but before sending the response 74 | * 75 | * @param Model $model 76 | * @return mixed The response to send or null 77 | */ 78 | protected function beforeShowResponse(Model $model) 79 | { 80 | } 81 | 82 | /** 83 | * Called for the response to method show() 84 | * 85 | * @param Model $model 86 | * @return Response|array 87 | */ 88 | abstract protected function showResponse(Model $model); 89 | } 90 | -------------------------------------------------------------------------------- /src/Controllers/Traits/Web.php: -------------------------------------------------------------------------------- 1 | success('Create successful'); 25 | } 26 | 27 | /** 28 | * StoreMany method success response 29 | * 30 | * @param array $data 31 | * @return Response 32 | */ 33 | protected function storeManyResponse(array $data) 34 | { 35 | return $this->success('Create many successful', Response::HTTP_CREATED); 36 | } 37 | 38 | /** 39 | * Update method success response 40 | * 41 | * @param Model $data 42 | * @return Response 43 | */ 44 | protected function updateResponse(Model $data) 45 | { 46 | return $this->success('Update successful'); 47 | } 48 | 49 | /** 50 | * Destroy method success response 51 | * 52 | * @param Model $data 53 | * @return Response 54 | */ 55 | protected function destroyResponse(Model $data) 56 | { 57 | return $this->success('Delete successful'); 58 | } 59 | 60 | /** 61 | * ForceDestroy method success response 62 | * 63 | * @param Model $data 64 | * @return Response 65 | */ 66 | protected function forceDestroyResponse(Model $data) 67 | { 68 | return $this->success('Permanent delete successful'); 69 | } 70 | 71 | /** 72 | * DestroyMany method success response 73 | * 74 | * @param int $deletedCount 75 | * @return Response 76 | */ 77 | protected function destroyManyResponse($deletedCount) 78 | { 79 | return $this->success("Deleted $deletedCount item(s) successfully"); 80 | } 81 | 82 | /** 83 | * RestoreDestroyed method success response 84 | * 85 | * @param Model $data 86 | * @return Response 87 | */ 88 | protected function restoreDestroyedResponse(Model $data) 89 | { 90 | return $this->success('Restoration successful'); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Helpers/Upload.php: -------------------------------------------------------------------------------- 1 | storeAs($path, $name); 24 | 25 | return $absoluteUrl ? url(Storage::url($storeUrl)) : $storeUrl; 26 | } 27 | 28 | /** 29 | * Uploads a file to the aws bucket storage 30 | * 31 | * @param UploadedFile $file The oploaded file 32 | * @param string $path The path to save the file to 33 | * @param boolean $absoluteUrl Indicates whether to return an absolute url to the upload file or not 34 | * @return bool|string 35 | */ 36 | public static function toS3Bucket(UploadedFile $file, $path = '', $absoluteUrl = true) 37 | { 38 | $name = self::createFilename($file); 39 | 40 | if (!self::s3()->put( 41 | $path . '/' . $name, 42 | fopen($file->getRealPath(), 'r+'), 'public') 43 | ) { 44 | return false; 45 | } 46 | 47 | $pathToFile = $path . '/' . $name; 48 | 49 | return $absoluteUrl ? self::s3url($pathToFile) : $pathToFile; 50 | } 51 | 52 | private static function createFileName(UploadedFile $file) : string 53 | { 54 | return md5(auth()->id() . time()) . '.' . $file->getClientOriginalExtension(); 55 | } 56 | 57 | /** 58 | * Shortcut to the s3 storage object 59 | */ 60 | public static function s3() 61 | { 62 | if (!self::$s3) { 63 | self::$s3 = Storage::disk('s3'); 64 | } 65 | 66 | return self::$s3; 67 | } 68 | 69 | /** 70 | * Returns the full url of the s3 bucket file path 71 | * 72 | * @param string $path The file path on s3 73 | * @return string 74 | */ 75 | public static function s3url($path) : string 76 | { 77 | $config = config('filesystems.disks.s3'); 78 | 79 | return '//s3.' . $config['region'] . '.amazonaws.com/' 80 | . $config['bucket'] . '/' . $path; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Models/Traits/Helper.php: -------------------------------------------------------------------------------- 1 | withoutGlobalScopes(!is_array($attributes) ? [$attributes] : $attributes); 21 | } 22 | 23 | /** 24 | * Excludes the given values from being selected from the database 25 | * Thanks to Ikechi Michael (@mykeels) 26 | * 27 | * @param Builder $query 28 | * @param string|array $value 29 | * @return Builder 30 | */ 31 | public function scopeExcept($query, $value): Builder 32 | { 33 | $defaultColumns = ['id', 'created_at', 'updated_at']; 34 | 35 | if (in_array('deleted_at', $this->dates)) { 36 | $defaultColumns[] = 'deleted_at'; 37 | } 38 | 39 | if (is_string($value)) { 40 | $value = [$value]; 41 | } 42 | 43 | return $query->select(array_diff(array_merge($defaultColumns, $this->fillable), (array) $value)); 44 | } 45 | 46 | /** 47 | * Removes timestamps from query 48 | * 49 | * @return self 50 | */ 51 | public function scopeWithoutTimestamps($query): self 52 | { 53 | $this->timestamps = false; 54 | 55 | return $query; 56 | } 57 | 58 | public function toArray(): array 59 | { 60 | $fillable = $this->fillable ?? []; 61 | $appends = $this->appends ?? []; 62 | $relations = array_keys($this->relations ?? []); 63 | $counts = array_keys($this->withCount ?? []); 64 | $withArray = $this->withArray ?? []; 65 | 66 | $relations = array_map(fn ($relation) => Str::snake($relation), $relations); 67 | $withArray = array_map(fn ($with) => Str::snake($with), $withArray); 68 | 69 | array_unshift($fillable, 'id'); 70 | $array = collect(parent::toArray()) 71 | // Show only fillables, appends and relations 72 | ->only(array_merge($fillable, $appends, $relations, $counts, $withArray)) 73 | ->all(); 74 | 75 | // remove nulls 76 | if (property_exists($this, 'arrayWithoutNulls') && $this->arrayWithoutNulls) { 77 | $array = array_filter($array); 78 | } 79 | 80 | return $array; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Jobs/AsyncCall.php: -------------------------------------------------------------------------------- 1 | callable = $callable; 38 | $this->arguments = $arguments; 39 | $this->callback = $callback; 40 | $this->callbackArguments = $callbackArguments; 41 | 42 | array_unshift($tags, 'async-call'); 43 | 44 | $this->tags = $tags; 45 | } 46 | 47 | /** 48 | * Execute the job. 49 | * 50 | * @return void 51 | */ 52 | public function handle() 53 | { 54 | $result = call_user_func_array($this->callable, $this->arguments); 55 | 56 | if ($this->callback) { 57 | array_unshift($this->callbackArguments, $result); 58 | call_user_func_array($this->callback, $this->callbackArguments); 59 | } 60 | } 61 | 62 | /** 63 | * The job failed to process. 64 | * 65 | * @param Exception $exception 66 | * @return void 67 | */ 68 | public function failed(Exception $ex) 69 | { 70 | if ($this->callback) { 71 | array_unshift($this->callbackArguments, $ex); 72 | call_user_func_array($this->callback, $this->callbackArguments); 73 | } 74 | } 75 | 76 | /** 77 | * Sets the tags for the job 78 | * 79 | * @return string|array 80 | */ 81 | public function tags() 82 | { 83 | return $this->tags; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Providers/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom($this->configPath(), 'laraquick'); 20 | 21 | Route::macro('httpResource', function ($path, $controller, array $options = []) { 22 | $only = $options['only'] ?? ['index', 'store', 'show', 'update', 'destroy']; 23 | $except = $options['except'] ?? []; 24 | $namePrefix = $options['namePrefix'] ?? preg_replace('[^a-zA-Z0-9]', '.', $path); 25 | $actionNames = ($options['actionNames'] ?? []) + [ 26 | 'index' => 'index', 27 | 'store' => 'store', 28 | 'show' => 'show', 29 | 'update' => 'update', 30 | 'destroy' => 'destroy', 31 | ]; 32 | 33 | if (!is_array($only)) { 34 | $only = [$only]; 35 | } 36 | 37 | if (!is_array($except)) { 38 | $except = [$except]; 39 | } 40 | 41 | if (in_array('index', $only) && !in_array('index', $except)) { 42 | $this->get($path, $controller . '@' . $actionNames['index'])->name("{$namePrefix}.index"); 43 | } 44 | if (in_array('store', $only) && !in_array('store', $except)) { 45 | $this->post($path, $controller . '@' . $actionNames['store'])->name("{$namePrefix}.store"); 46 | } 47 | if (in_array('show', $only) && !in_array('show', $except)) { 48 | $this->get($path . '/{id}', $controller . '@' . $actionNames['show'])->name("{$namePrefix}.show"); 49 | } 50 | if (in_array('update', $only) && !in_array('update', $except)) { 51 | $this->put($path . '/{id}', $controller . '@' . $actionNames['update'])->name("{$namePrefix}.update"); 52 | } 53 | if (in_array('destroy', $only) && !in_array('destroy', $except)) { 54 | $this->delete($path . '/{id}', $controller . '@' . $actionNames['destroy'])->name("{$namePrefix}.destroy"); 55 | } 56 | 57 | return $this; 58 | }); 59 | } 60 | 61 | /** 62 | * Register the application services. 63 | * 64 | * @return void 65 | */ 66 | public function boot() 67 | { 68 | $this->publishes([ 69 | $this->configPath() => config_path('laraquick.php'), 70 | ], 'laraquick-config'); 71 | } 72 | 73 | protected function configPath(): string 74 | { 75 | return dirname(__DIR__) . "/config/laraquick.php"; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Controllers/Traits/Crud/Crud.php: -------------------------------------------------------------------------------- 1 | model(); 29 | } 30 | 31 | /** 32 | * The model to use in the index method when @see searchQueryParam() exists in the `GET` query. 33 | * 34 | * It should return the model after the query conditions have been implemented. 35 | * Defaults to @see indexModel() 36 | * 37 | * @return mixed 38 | */ 39 | protected function searchModel($query) 40 | { 41 | return $this->indexModel(); 42 | } 43 | 44 | /** 45 | * The model to use in the store method. Defaults to @see model() 46 | * 47 | * @return mixed 48 | */ 49 | protected function storeModel() 50 | { 51 | return $this->model(); 52 | } 53 | 54 | /** 55 | * The model to use in the show method. Defaults to @see model() 56 | * 57 | * @return mixed 58 | */ 59 | protected function showModel() 60 | { 61 | return $this->model(); 62 | } 63 | 64 | 65 | /** 66 | * The model to use in the update method. Defaults to @see model() 67 | * 68 | * @return mixed 69 | */ 70 | protected function updateModel() 71 | { 72 | return $this->showModel(); 73 | } 74 | 75 | /** 76 | * The model to use in the delete method. Defaults to @see model() 77 | * 78 | * @return mixed 79 | */ 80 | protected function destroyModel() 81 | { 82 | return $this->showModel(); 83 | } 84 | 85 | /** 86 | * Handles finding an entity from a model 87 | * 88 | * @param Model|string $model 89 | * @param int|string $id 90 | */ 91 | protected function find($model, $id) 92 | { 93 | return is_object($model) ? $model->find($id) : $model::find($id); 94 | } 95 | 96 | /** 97 | * Handles finding an entity from a model and throws an exception if not found 98 | * 99 | * @param Model|string $model 100 | * @param int|string $id 101 | */ 102 | protected function findOrFail($model, $id) 103 | { 104 | return is_object($model) ? $model->findOrFail($id) : $model::findOrFail($id); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Models/Traits/Searchable.php: -------------------------------------------------------------------------------- 1 | = 3) { 39 | $word = '+' . $word . '*'; 40 | } 41 | } 42 | 43 | return implode(' ', $words); 44 | } 45 | 46 | /** 47 | * Sets the name of the relevance score field 48 | * 49 | * @return string 50 | */ 51 | protected function relevanceScoreName(): string 52 | { 53 | return 'relevance_score'; 54 | } 55 | 56 | /** 57 | * Fetch the searchable columns 58 | * 59 | * @return array 60 | */ 61 | protected function searchableColumns(): array 62 | { 63 | return $this->searchable ?? []; 64 | } 65 | 66 | /** 67 | * Perform a full text search on the given text 68 | * 69 | * @param Builder $query 70 | * @param string $text 71 | * @return Builder 72 | */ 73 | public function scopeSearch(Builder $query, $text): Builder 74 | { 75 | if ($text) { 76 | $columns = '`' . $this->getTable() . '`.`' 77 | . implode("`, `{$this->getTable()}`.`", $this->searchableColumns()) 78 | . '`'; 79 | 80 | $this->fullTextMatchString = "MATCH ({$columns}) AGAINST (? IN BOOLEAN MODE)"; 81 | $query->whereRaw($this->fullTextMatchString, $this->treat($text)); 82 | 83 | if (count($this->orderParams)) { 84 | $this->scopeOrderByRelevance($query, ...$this->orderParams); 85 | } 86 | } 87 | 88 | return $query; 89 | } 90 | 91 | /** 92 | * Perform a full text search according to relevance scores 93 | * 94 | * @param Builder $query 95 | * @param string $direction 96 | * @param string $relevanceScoreName 97 | * @return Builder 98 | */ 99 | public function scopeOrderByRelevance(Builder $query, $direction = 'desc', $relevanceScoreName = null): Builder 100 | { 101 | if (!$this->fullTextMatchString) { 102 | $this->orderParams = [$direction, $relevanceScoreName]; 103 | } else { 104 | $relevanceScoreName = $relevanceScoreName ?: $this->relevanceScoreName(); 105 | $query->selectRaw($this->fullTextMatchString . " AS {$relevanceScoreName}") 106 | ->orderBy($relevanceScoreName, $direction); 107 | } 108 | 109 | return $query; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Controllers/Traits/Crud/Validation.php: -------------------------------------------------------------------------------- 1 | fails()) { 23 | throw new ValidationException($validator); 24 | } 25 | 26 | return $validator->validated(); 27 | } 28 | 29 | /** 30 | * Checks the request data against validation rules 31 | * 32 | * @param array $rules 33 | * @param array $messages 34 | * @return array 35 | */ 36 | protected function validateRequest(array $rules = null, array $messages = null): array 37 | { 38 | if (!empty($this->validationRequest())) { 39 | $request = app($this->validationRequest()); 40 | 41 | return $request->validated(); 42 | } 43 | 44 | $data = request()->all(); 45 | 46 | return $this->validateData( 47 | $data, 48 | $rules ?: $this->validationRules($data), 49 | $messages ?: $this->validationMessages($data) 50 | ); 51 | } 52 | 53 | protected function validationRequest(): string 54 | { 55 | return ''; 56 | } 57 | 58 | /** 59 | * Should return the validation rules for when using @see store() and @see update(). 60 | * 61 | * @param array $data The data being validated 62 | * @param mixed $id Id of the model being updated, if such were the case 63 | * @return array 64 | */ 65 | protected function validationRules(array $data, $id = null): array 66 | { 67 | return []; 68 | } 69 | 70 | /** 71 | * Should return the validation rules for when using @see storeMany() 72 | * 73 | * @param array $data The data being validated 74 | * @param mixed $id Id of the model being updated, if such were the case 75 | * @return array 76 | */ 77 | final protected function manyValidationRules(array $data, $id = null): array 78 | { 79 | $rules = $this->validationRules($data, $id); 80 | 81 | return $this->manyrize($rules); 82 | } 83 | 84 | /** 85 | * The validation messages to use with the @see validationRules() 86 | * 87 | * @param array $data The data being validated 88 | * @param mixed $id The id of the model being updated, if such were the case 89 | * @return array 90 | */ 91 | protected function validationMessages(array $data, $id = null): array 92 | { 93 | return []; 94 | } 95 | 96 | /** 97 | * The validation messages to use with the @see manyValidationRules() 98 | * 99 | * @param array $data The data being validated 100 | * @param mixed $id Id of the model being updated, if such were the case 101 | * @return array 102 | */ 103 | final protected function manyValidationMessages(array $data, $id = null): array 104 | { 105 | $messages = $this->validationMessages($data, $id); 106 | 107 | return $this->manyrize($messages); 108 | } 109 | 110 | private function manyrize($array): array 111 | { 112 | $manyrized = []; 113 | 114 | foreach ($array as $key => $value) { 115 | $manyrized['many.*.' . $key] = $value; 116 | } 117 | 118 | return $manyrized; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Controllers/Traits/Response/Common.php: -------------------------------------------------------------------------------- 1 | success($resp, $code, $meta); 29 | } 30 | 31 | /** 32 | * Called when validation fails 33 | * 34 | * @param mixed $errors 35 | * @return JsonResponse 36 | */ 37 | protected function validationError($errors) 38 | { 39 | return $this->error('Validation error', $errors); 40 | } 41 | 42 | /** 43 | * Should be called when an error occurred which is not a fault of the user's 44 | * 45 | * @param string $message The message to send with a 500 status code 46 | * @return JsonResponse 47 | */ 48 | protected function serverError($message) 49 | { 50 | return $this->error($message, null, 500); 51 | } 52 | 53 | /** 54 | * Called when create action fails 55 | * 56 | * @param string $message The message to send with a 500 status code 57 | * @return JsonResponse 58 | */ 59 | protected function storeFailedError($message = 'Create failed') 60 | { 61 | return $this->serverError($message); 62 | } 63 | 64 | /** 65 | * Error message for when an update action fails 66 | * 67 | * @param string $message The message to send with a 500 status code 68 | * @return JsonResponse 69 | */ 70 | protected function updateFailedError($message = 'Update failed') 71 | { 72 | return $this->serverError($message); 73 | } 74 | 75 | /** 76 | * Called when a delete action fails 77 | * 78 | * @param string $message The message to send with a 500 status code 79 | * @return JsonResponse 80 | */ 81 | protected function destroyFailedError($message = 'Delete failed') 82 | { 83 | return $this->serverError($message); 84 | } 85 | 86 | /** 87 | * Called when a restore deleted action fails 88 | * 89 | * @param string $message The message to send with a 500 status code 90 | * @return JsonResponse 91 | */ 92 | protected function restoreFailedError($message = 'Restoration failed') 93 | { 94 | return $this->serverError($message); 95 | } 96 | 97 | /** 98 | * Create a 404 not found error response 99 | * 100 | * @param string $message The message to send with a 404 status code 101 | * @return JsonResponse 102 | */ 103 | protected function notFoundError($message = 'Resource not found') 104 | { 105 | return $this->error($message, null, 404); 106 | } 107 | 108 | /** 109 | * Create a model not set error response 110 | * 111 | * @param string $message The message to send with a 500 status code 112 | * @return JsonResponse 113 | */ 114 | protected function modelNotSetError($message = 'Model not set for action') 115 | { 116 | return $this->serverError($message); 117 | } 118 | 119 | /** 120 | * Translates a given text 121 | * 122 | * @param string $text 123 | * @return string 124 | */ 125 | protected function translate(string $text): string 126 | { 127 | return trans($text); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Docs/Scribe/Strategies/Responses/UseTestResponses.php: -------------------------------------------------------------------------------- 1 | getTestResponseFiles($endpointData); 36 | 37 | foreach ($responseFiles as $responseFile) { 38 | $docs[] = [ 39 | 'status' => (int) basename($responseFile), 40 | 'content' => Storage::get($responseFile), 41 | ]; 42 | } 43 | 44 | return $docs; 45 | } 46 | 47 | private function getTestResponseFiles(ExtractedEndpointData $endpointData): array 48 | { 49 | if (config('laraquick.tests.responses.format') !== 'json') { 50 | return []; 51 | } 52 | 53 | $testResponsesPath = config('laraquick.tests.responses.storage_path'); 54 | 55 | $path = $this->getPathFromResponsePaths($endpointData) ?? $this->generatePath($endpointData); 56 | $fullPath = $testResponsesPath . DIRECTORY_SEPARATOR . ($path ?? ''); 57 | 58 | if (is_null($path) || !Storage::exists($fullPath)) { 59 | return []; 60 | } 61 | 62 | return Storage::allFiles($fullPath); 63 | } 64 | 65 | private function getPathFromResponsePaths(ExtractedEndpointData $endpointData): ?string 66 | { 67 | if (!$endpointData->controller->hasMethod('responsePaths')) { 68 | return null; 69 | } 70 | 71 | $paths = $endpointData->controller->newInstanceWithoutConstructor()->responsePaths(); 72 | 73 | if (is_array($paths)) { 74 | return $paths[$endpointData->method->getName()] ?? null; 75 | } 76 | 77 | return null; 78 | } 79 | 80 | private function generatePath(ExtractedEndpointData $endpointData): string 81 | { 82 | [$controllerName, $methodName] = Utils::getRouteClassAndMethodNames($endpointData->route); 83 | 84 | if (!is_string($controllerName)) { 85 | return []; 86 | } 87 | 88 | $controllerResourceName = $this->getControllerResourceName($controllerName); 89 | 90 | return $controllerResourceName . DIRECTORY_SEPARATOR . Str::kebab($methodName); 91 | } 92 | 93 | private function getControllerResourceName(string $controllerName): string 94 | { 95 | return Str::kebab( 96 | Str::before( 97 | basename( 98 | str_replace('\\', DIRECTORY_SEPARATOR, $controllerName) 99 | ), 100 | 'Controller' 101 | ) 102 | ); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Controllers/Traits/Crud/Update.php: -------------------------------------------------------------------------------- 1 | updateModel(); 77 | 78 | $requestData = $request->all(); 79 | $data = $this->validateRequest($this->validationRules($requestData, $id), $this->validationMessages($requestData, $id)); 80 | 81 | $item = $this->find($model, $id); 82 | 83 | if (!$item) { 84 | return $this->notFoundError(); 85 | } 86 | 87 | if (method_exists($this, 'authorizeMethod')) { 88 | $this->authorizeMethod('update', [$model, $item]); 89 | } 90 | 91 | return DB::transaction( 92 | function () use (&$data, &$item) { 93 | if ($resp = $this->beforeUpdate($data, $item)) { 94 | return $resp; 95 | } 96 | 97 | $result = $item->update($data); 98 | 99 | if (!$result) { 100 | throw new \Exception('Update method returned falsable'); 101 | } 102 | 103 | if ($resp = $this->beforeUpdateResponse($item)) { 104 | return $resp; 105 | } 106 | 107 | return $this->updateResponse($item); 108 | }, 109 | function ($ex) use ($data, $item) { 110 | $message = $ex->getMessage(); 111 | 112 | try { 113 | $this->rollbackUpdate($data, $item); 114 | } catch (\Exception $ex) { 115 | $message = $ex->getMessage(); 116 | } 117 | 118 | return $this->updateFailedError($message); 119 | } 120 | ); 121 | } 122 | 123 | /** 124 | * Called on success but before sending the response 125 | * 126 | * @param Model $model 127 | * @return mixed The response to send or null 128 | */ 129 | protected function beforeUpdateResponse(Model $model) {} 130 | 131 | /** 132 | * Called for the response to method update() 133 | * 134 | * @param Model $model 135 | * @return Response|array 136 | */ 137 | abstract protected function updateResponse(Model $model); 138 | } 139 | -------------------------------------------------------------------------------- /src/Controllers/Traits/Api.php: -------------------------------------------------------------------------------- 1 | modelResource())) { 33 | if (is_array($data)) { 34 | foreach ($data as &$item) { 35 | $item = app($this->modelResource(), ['resource' => $item]); 36 | } 37 | } else { 38 | $data->getCollection()->transform(fn($item) => app($this->modelResource(), ['resource' => $item])); 39 | } 40 | } 41 | 42 | return $this->paginatedList(is_array($data) ? $data : $data->toArray()); 43 | } 44 | 45 | /** 46 | * StoreMany method success response 47 | * 48 | * @param array $data 49 | * @return JsonResponse 50 | */ 51 | protected function storeManyResponse(array $data) 52 | { 53 | if (!empty($this->modelResource())) { 54 | foreach ($data as &$item) { 55 | $item = app($this->modelResource(), ['resource' => $item]); 56 | } 57 | } 58 | 59 | return $this->success($data, Response::HTTP_CREATED); 60 | } 61 | 62 | /** 63 | * Store method success response 64 | * 65 | * @param Model $model 66 | * @return JsonResponse 67 | */ 68 | protected function storeResponse(Model $model) 69 | { 70 | return $this->success( 71 | !empty($this->modelResource()) ? app($this->modelResource(), ['resource' => $model]) : $model, 72 | Response::HTTP_CREATED 73 | ); 74 | } 75 | 76 | /** 77 | * Show method success response 78 | * 79 | * @param Model $model 80 | * @return JsonResponse 81 | */ 82 | protected function showResponse(Model $model) 83 | { 84 | return $this->success( 85 | !empty($this->modelResource()) ? app($this->modelResource(), ['resource' => $model]) : $model, 86 | Response::HTTP_OK 87 | ); 88 | } 89 | 90 | /** 91 | * Update method success response 92 | * 93 | * @param Model $model 94 | * @return JsonResponse 95 | */ 96 | protected function updateResponse(Model $model) 97 | { 98 | return $this->success( 99 | !empty($this->modelResource()) ? app($this->modelResource(), ['resource' => $model]) : $model, 100 | Response::HTTP_ACCEPTED 101 | ); 102 | } 103 | 104 | /** 105 | * Destroy method success response 106 | * 107 | * @param Model $model 108 | * @return JsonResponse 109 | */ 110 | protected function destroyResponse(Model $model) 111 | { 112 | return $this->success( 113 | !empty($this->modelResource()) ? app($this->modelResource(), ['resource' => $model]) : $model, 114 | Response::HTTP_ACCEPTED 115 | ); 116 | } 117 | 118 | /** 119 | * ForceDestroy method success response 120 | * 121 | * @param Model $model 122 | * @return JsonResponse 123 | */ 124 | protected function forceDestroyResponse(Model $model) 125 | { 126 | return $this->success( 127 | !empty($this->modelResource()) ? app($this->modelResource(), ['resource' => $model]) : $model, 128 | Response::HTTP_ACCEPTED 129 | ); 130 | } 131 | 132 | /** 133 | * DestroyMany method success response 134 | * 135 | * @param int $deletedCount 136 | * @return JsonResponse 137 | */ 138 | protected function destroyManyResponse($deletedCount) 139 | { 140 | return $this->success("$deletedCount item(s) deleted successfully", Response::HTTP_ACCEPTED); 141 | } 142 | 143 | /** 144 | * RestoreDestroyed method success response 145 | * 146 | * @param Model $model 147 | * @return JsonResponse 148 | */ 149 | protected function restoreDestroyedResponse(Model $model) 150 | { 151 | return $this->success( 152 | !empty($this->modelResource()) ? app($this->modelResource(), ['resource' => $model]) : $model, 153 | Response::HTTP_ACCEPTED 154 | ); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 6.3.11 4 | 5 | Changes Strategies MetaData directory name to Metadata 6 | 7 | ## 6.3.10 8 | 9 | - Allows picking documentation test response paths for a controller from method `responsePaths` 10 | - Reorganises Scribe strategies into directories for clearer understanding 11 | - Deprecates former Scribe strategies 12 | 13 | ## 6.3.9 14 | 15 | Fixes issue with Config::s3 method 16 | 17 | 18 | ## 6.3.8 19 | 20 | Defaults the second param of Config::s3 to empty string 21 | 22 | ## 6.3.7 23 | 24 | Fixes preg_replace null third param deprecation 25 | 26 | ## 6.3.6 27 | 28 | Adds `url` to config generated by `Config::s3` 29 | 30 | ## 6.3.5 31 | 32 | Fixes `Config::s3` paths with missing `DIRECTORY_SEPARATOR` 33 | 34 | ## 6.3.4 35 | 36 | Fixes a bug with `Config::s3` 37 | 38 | ## 6.3.3 39 | 40 | ### Added 41 | 42 | - Adds method `modelResource` for API controllers 43 | 44 | ### Updated 45 | 46 | - Fixes `paginatedList` for arrays 47 | 48 | ## 6.3.2 49 | 50 | Fixes a typo bug 51 | 52 | ## 6.3.1 53 | 54 | ### Removed 55 | 56 | - Makes using Authorize trait optional 57 | 58 | ## 6.2.0, 6.3.0 59 | 60 | ### Added 61 | 62 | - Adds method `validationRequest` for controllers. 63 | - Removed deprecated `Upload::awsUpload` method. 64 | 65 | ## 6.1.0 66 | 67 | ### Added 68 | 69 | - Adds `Config` helper class 70 | 71 | ## 6.0.0 72 | 73 | ### Added 74 | 75 | - Adds method `getMailMessage` to Notifications\Send. 76 | - Adds [Scribe](https://scribe.knuckles.wtf/laravel) strategy for test responses. 77 | - Adds [Scribe](https://scribe.knuckles.wtf/laravel) strategy for metadata. 78 | - Adds documentation doctype to all crud methods 79 | - Introduces "actionNames" options for `Route::httpResource(...)` 80 | 81 | ### Updated 82 | 83 | - Fixed toArray issue with snake/camel case relationships. 84 | - Made the validationRules method not required. It returns an empty array by default. 85 | - Changes test response path to reflect status codes. 86 | - Swaps [d-scribe/laravel-apidoc-generator](https://github.com/d-scribe/laravel-apidoc-generator) with [Scribe](https://scribe.knuckles.wtf/laravel). 87 | - Changes 'all' to `-1` on index action to load all. 88 | - Fixes storing test responses with 204 content 89 | - Renames config tag from just 'config' to 'laraquick-config' 90 | - Updates the indexResponse param type 91 | - Uses Laravel's default 'trans' function in the translate method 92 | 93 | ### Removed 94 | 95 | - Removed Helper/Http. Laravel now has an Http facade. 96 | - Removed Events/WebSocket. Use [Laravel WebSockets](https://beyondco.de/docs/laravel-websockets/getting-started/introduction). 97 | - Removed Helper::relationLoaded method. Laravel now has a method for that. 98 | - Removed Helper/Excel. 99 | - Removed [d-scribe/laravel-db-command](https://github.com/ezra-obiwale/laravel-db-command) as a dependency. 100 | - Removed JWT references entirely 101 | 102 | ## Missed out on a couple of version. Will try to keep this file up-to-date going forward. 103 | 104 | ## 3.8.0 105 | 106 | Added Notfications DBChannel 107 | 108 | ## 3.6.2 109 | 110 | - Fixed issue with `before{method}Response` in PassThrough's respond method 111 | 112 | ## 3.6.0 113 | 114 | - Added DB helper class to create full text indexes 115 | - Added Searchable trait to provide methods for full text searches 116 | 117 | ## 3.5.10 118 | 119 | - Ensure `0` and empty strings are allowed as success and error messages 120 | 121 | ## 3.5.7 122 | 123 | - Updated parameters for method `rollbackStore()` 124 | - Updated method `rollbackDestroy()` 125 | - Catch and log exceptions thrown in `rollbackDestroy()`. 126 | 127 | ## 3.5.6 128 | 129 | - Catch and log exceptions thrown in methods `rollbackStore()` and `rollbackUpdate()`. 130 | 131 | ## 3.5.5 132 | 133 | - Updated methods `rollbackStore()` and `rollbackUpdate()`. 134 | 135 | ## 3.4.1 136 | 137 | - Fixed typo 138 | 139 | ## 3.4.0 140 | 141 | - Added method `validateData()` 142 | - Method `validateRequest()` now only takes parameters `$rules` and `$messages`. 143 | - Implemented using custom validation messages 144 | - Deprecated method `checkRequestData()` should be replaced with new method `validateData()` 145 | - Removed parameter `$ignoreStrict` entirely from method `validateData()` 146 | 147 | ## 3.3.4 148 | 149 | Deprecated method `checkRequestData()` in favour of `validateRequest()` 150 | 151 | ## 3.3.3 152 | 153 | - Ensured exceptions in `beforeResponse` methods are caught 154 | - Ensured method `rollback` is called on the caught errors 155 | 156 | ## 3.3.1 157 | 158 | Fixed notFoundError consistency issue' 159 | 160 | ## 3.3.0 161 | 162 | - Exempted `$hidden` attributes in model helper trait 163 | - Added method `validationMessages()` to the validation trait 164 | - Used `validationMessages()` in both storing and updating. 165 | - Allowed custom `notFoundError` messages 166 | - Allowed success method in api to have zero params 167 | 168 | ## 3.2.0 169 | 170 | - Check that model methods do not return falsable 171 | - The model being updated is now passed as the second parameter to `beforeUpdate` 172 | 173 | ## 3.1.0 174 | 175 | `Laraquick\Models\Traits\Helper` now has an `except` scope method to remove provided 176 | columns from selection. Thanks [@mykeels](https://github.com/mykeels). 177 | 178 | ## 3.0.5 179 | 180 | Response method `paginatedList()` now takes a third parameter, an array 181 | of custom meta fields. 182 | 183 | ## 3.0.4 184 | 185 | Created Dud model 186 | 187 | PassThrough Trait: 188 | - Updated methed names with *create* to *store* 189 | - Updated method names with *delete* to *destroy* 190 | - Ensured using validation rules work as expected 191 | 192 | ## 3.0.3 193 | 194 | Made attach/detach/sync error message more custom to the parameter key or relation 195 | name 196 | 197 | Added methods `prepareAttachItems`, `prepareDetachItems`, `prepareSyncItems` to **Attachable** 198 | 199 | ## 3.0.2 200 | 201 | Converted **Attachable** public methods' `$relation` to camelCase if it doesn't exist 202 | on the model in the default form. 203 | 204 | ## 3.0.1 205 | 206 | Created model helper trait 207 | 208 | ## 3.0.0 209 | 210 | Changed response structure to: 211 | 212 | ```javascript 213 | { 214 | "status": "ok", // or "error", if the request failed 215 | "message": "...", // [optional] holds a string description of the status 216 | "data": "", // [optional] holds the retrieved/processed data if any 217 | "errors": [] // [optional] holds the errors encountered while processing the request 218 | } 219 | ``` -------------------------------------------------------------------------------- /src/Notifications/Send.php: -------------------------------------------------------------------------------- 1 | mailMessage = $mailMessage; 74 | $this->databaseData = $databaseData; 75 | $this->broadcastData = $broadcastData; 76 | } 77 | 78 | /** 79 | * Sets the mail message 80 | * 81 | * @param MailMessage $mailMessage 82 | * @return self 83 | */ 84 | public function setMailMessage(MailMessage $mailMessage) 85 | { 86 | $this->mailMessage = $mailMessage; 87 | 88 | return $this; 89 | } 90 | 91 | /** 92 | * Gets the mail message 93 | * 94 | * @return MailMessage 95 | */ 96 | public function getMailMessage() 97 | { 98 | return $this->mailMessage; 99 | } 100 | 101 | /** 102 | * Sets the array of data for saving to the database. 103 | * 104 | * @param array $data 105 | * @return self 106 | */ 107 | public function setDatabaseData(array $data) 108 | { 109 | $this->databaseData = $data; 110 | 111 | return $this; 112 | } 113 | 114 | /** 115 | * Sets the array of data to use for broadcasting. 116 | * 117 | * @param array $data 118 | * @return self 119 | */ 120 | public function setBroadcastData(array $data) 121 | { 122 | $this->broadcastData = $data; 123 | 124 | return $this; 125 | } 126 | 127 | /** 128 | * Sets the channels the event should be broadcast on 129 | * 130 | * @param array $channels 131 | * @return self 132 | */ 133 | public function setBroadcastChannels(array $channels) 134 | { 135 | $this->broadcastChannels = $channels; 136 | 137 | return $this; 138 | } 139 | 140 | /** 141 | * Sets the broadcast message to be sent to the event channels 142 | * 143 | * @param BroadcastMessage $message 144 | * @return self 145 | */ 146 | public function setBroadcastMessage(BroadcastMessage $message) 147 | { 148 | $this->broadcastMessage = $message; 149 | 150 | return $this; 151 | } 152 | 153 | /** 154 | * Sets the function to create the broadcast message object. The function receives the notifiable as the only parameter. 155 | * 156 | * @param callable $creator 157 | * @return self 158 | */ 159 | public function createsBroadcastMessage(callable $creator) 160 | { 161 | $this->broadcastMessageCreator = $creator; 162 | 163 | return $this; 164 | } 165 | 166 | /** 167 | * Sets a function to create the mail message object. The function receives the notifiable as the only parameter. 168 | * 169 | * @param callable $creator 170 | * @return self 171 | */ 172 | public function createMailMessage(callable $creator) 173 | { 174 | $this->mailMessageCreator = $creator; 175 | 176 | return $this; 177 | } 178 | 179 | /** 180 | * Get the notification's delivery channels. 181 | * 182 | * @param mixed $notifiable 183 | * @return array 184 | */ 185 | public function via($notifiable) 186 | { 187 | $via = []; 188 | 189 | if ($this->toMail($notifiable)) { 190 | $via[] = 'mail'; 191 | } 192 | 193 | if (count($this->databaseData)) { 194 | $via[] = 'database'; 195 | } 196 | 197 | return $via; 198 | } 199 | 200 | /** 201 | * Get the mail representation of the notification. 202 | * 203 | * @param mixed $notifiable 204 | * @return \Illuminate\Notifications\Messages\MailMessage 205 | */ 206 | public function toMail($notifiable) 207 | { 208 | return $this->mailMessageCreator ? call_user_func($this->mailMessageCreator, $notifiable) : $this->mailMessage; 209 | } 210 | 211 | /** 212 | * Get the array representation of the notification. 213 | * 214 | * @param mixed $notifiable 215 | * @return array 216 | */ 217 | public function toDatabase($notifiable) 218 | { 219 | return $this->databaseData; 220 | } 221 | 222 | /** 223 | * Get the array representation of the notification. 224 | * 225 | * @param mixed $notifiable 226 | * @return array 227 | */ 228 | public function toArray($notifiable) 229 | { 230 | return $this->broadcastData; 231 | } 232 | 233 | /** 234 | * Get the broadcastable representation of the notification. 235 | * 236 | * @param mixed $notifiable 237 | * @return BroadcastMessage 238 | */ 239 | public function toBroadcast($notifiable) 240 | { 241 | return $this->broadcastMessageCreator ? call_user_func($this->broadcastMessageCreator, $notifiable) : $this->broadcastMessage; 242 | } 243 | 244 | /** 245 | * Get the channels the event should broadcast on. 246 | * 247 | * @return array 248 | */ 249 | public function broadcastOn() 250 | { 251 | return $this->broadcastChannels; 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/Controllers/Traits/Helpers/Attachable.php: -------------------------------------------------------------------------------- 1 | model(); 25 | } 26 | 27 | /** 28 | * The model to use in the detach method. Defaults to @see model() 29 | * 30 | * @return mixed 31 | */ 32 | protected function detachModel() 33 | { 34 | return $this->model(); 35 | } 36 | 37 | /** 38 | * The model to use in the sync method. Defaults to @see model() 39 | * 40 | * @return mixed 41 | */ 42 | protected function syncModel() 43 | { 44 | return $this->model(); 45 | } 46 | 47 | /** 48 | * Treats the relation string 49 | * 50 | * @param Model $model 51 | * @param string $relation 52 | * @return string 53 | */ 54 | private function treatRelation(Model $model, &$relation): string 55 | { 56 | if (!method_exists($model, $relation)) { 57 | // change relation to camel case 58 | $relation = camel_case(str_replace('-', '_', $relation)); 59 | } 60 | 61 | return $relation; 62 | } 63 | 64 | /** 65 | * Prepares the items to attach to the model on the given relation 66 | * 67 | * @param mixed $items 68 | * @param Model $model 69 | * @param string $relation 70 | * @return mixed 71 | */ 72 | protected function prepareAttachItems($items, Model $model, $relation) 73 | { 74 | return $items; 75 | } 76 | 77 | /** 78 | * Prepares the items to detach to the model on the given relation 79 | * 80 | * @param mixed $items 81 | * @param Model $model 82 | * @param string $relation 83 | * @return mixed 84 | */ 85 | protected function prepareDetachItems($items, Model $model, $relation) 86 | { 87 | return $items; 88 | } 89 | 90 | /** 91 | * Prepares the items to sync to the model on the given relation 92 | * 93 | * @param mixed $items 94 | * @param Model $model 95 | * @param string $relation 96 | * @return mixed 97 | */ 98 | protected function prepareSyncItems($items, Model $model, $relation) 99 | { 100 | return $items; 101 | } 102 | 103 | /** 104 | * Fetches a paginated list of related items 105 | * 106 | * @param mixed $id 107 | * @param string $relation 108 | * @return Response 109 | */ 110 | public function attached($id, $relation) 111 | { 112 | $model = $this->attachModel(); 113 | $model = is_object($model) 114 | ? $model->find($id) 115 | : $model::find($id); 116 | 117 | if (!$model) { 118 | return $this->notFoundError(); 119 | } 120 | 121 | $this->treatRelation($model, $relation); 122 | $list = $model->$relation()->paginate(); 123 | 124 | return $this->paginatedList($list->toArray()); 125 | } 126 | 127 | /** 128 | * Attaches a list of items to the object at the given id 129 | * 130 | * @param int $id 131 | * @param string $relation 132 | * @param string $paramKey 133 | * @return Response 134 | */ 135 | public function attach($id, $relation, $paramKey = null) 136 | { 137 | $paramKey = $paramKey ?: $relation; 138 | 139 | $items = $this->validateRequest([ 140 | $paramKey => 'required|array' 141 | ]); 142 | 143 | $model = $this->attachModel(); 144 | $model = is_object($model) 145 | ? $model->find($id) 146 | : $model::find($id); 147 | 148 | if (!$model) { 149 | return $this->notFoundError(); 150 | } 151 | 152 | try { 153 | $this->treatRelation($model, $relation); 154 | $model->$relation()->syncWithoutDetaching($this->prepareAttachItems($items, $model, $relation)); 155 | 156 | return $this->success($model->load($relation)->$relation); 157 | } catch (\Exception $e) { 158 | Log::error($e->getMessage()); 159 | 160 | return $this->error('Something went wrong. Are you sure the ' . str_replace('_', ' ', $paramKey) . ' exists?'); 161 | } 162 | } 163 | 164 | /** 165 | * Detaches a list of items from the object at the given id 166 | * 167 | * @param int $id 168 | * @param string $relation 169 | * @param string $paramKey 170 | * @return Response 171 | */ 172 | public function detach($id, $relation, $paramKey = null) 173 | { 174 | $paramKey = $paramKey ?: $relation; 175 | 176 | $items = $this->validateRequest([ 177 | $paramKey => 'required|array' 178 | ]); 179 | 180 | $model = $this->detachModel(); 181 | $model = is_object($model) 182 | ? $model->find($id) 183 | : $model::find($id); 184 | 185 | if (!$model) { 186 | return $this->notFoundError(); 187 | } 188 | 189 | try { 190 | $this->treatRelation($model, $relation); 191 | $_items = $model->$relation()->find($items); 192 | $model->$relation()->detach($this->prepareDetachItems($items, $model, $relation)); 193 | 194 | return $this->success($_items); 195 | } catch (\Exception $e) { 196 | Log::error($e->getMessage()); 197 | 198 | return $this->error('Something went wrong. Are you sure the ' . str_replace('_', ' ', $paramKey) . ' exists?'); 199 | } 200 | } 201 | 202 | /** 203 | * Syncs a list of items with the existing attached items on the object at the given id 204 | * 205 | * @param int $id 206 | * @param string $relation 207 | * @param string $paramKey 208 | * @return Response 209 | */ 210 | public function sync($id, $relation, $paramKey = null) 211 | { 212 | $paramKey = $paramKey ?: $relation; 213 | 214 | $items = $this->validateRequest([ 215 | $paramKey => 'required|array' 216 | ]); 217 | 218 | $model = $this->syncModel(); 219 | $model = is_object($model) 220 | ? $model->find($id) 221 | : $model::find($id); 222 | 223 | if (!$model) { 224 | return $this->notFoundError(); 225 | } 226 | 227 | try { 228 | $this->treatRelation($model, $relation); 229 | 230 | $resp = $model->$relation()->sync($this->prepareSyncItems($items, $model, $relation)); 231 | $resp['added'] = $resp['attached']; 232 | $resp['removed'] = $resp['detached']; 233 | unset($resp['attached']); 234 | unset($resp['detached']); 235 | 236 | return $this->success($model->load($relation)->$relation); 237 | } catch (\Exception $e) { 238 | Log::error($e->getMessage()); 239 | return $this->error('Something went wrong. Are you sure the ' . str_replace('_', ' ', $paramKey) . ' exists?'); 240 | } 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /src/Controllers/Traits/PassThrough.php: -------------------------------------------------------------------------------- 1 | http) { 25 | $this->http = new Http; 26 | } 27 | return $this->http; 28 | } 29 | 30 | public function model() 31 | { 32 | } 33 | 34 | /** 35 | * The headers to pass into requests 36 | * 37 | * @return array 38 | */ 39 | abstract protected function headers(): array; 40 | 41 | /** 42 | * The url to pass the request to 43 | * 44 | * @return string 45 | */ 46 | abstract protected function toUrl(): string; 47 | 48 | /** 49 | * Headers to send with index request 50 | * 51 | * @return array 52 | */ 53 | protected function indexHeaders(): array 54 | { 55 | return $this->headers(); 56 | } 57 | 58 | /** 59 | * Headers to send with store request 60 | * 61 | * @return array 62 | */ 63 | protected function storeHeaders(): array 64 | { 65 | return $this->headers(); 66 | } 67 | 68 | /** 69 | * Headers to send with show request 70 | * 71 | * @return array 72 | */ 73 | protected function showHeaders(): array 74 | { 75 | return $this->headers(); 76 | } 77 | 78 | /** 79 | * Headers to send with update request 80 | * 81 | * @return array 82 | */ 83 | protected function updateHeaders(): array 84 | { 85 | return $this->headers(); 86 | } 87 | 88 | /** 89 | * Headers to send with destroy request 90 | * 91 | * @return array 92 | */ 93 | protected function destroyHeaders(): array 94 | { 95 | return $this->headers(); 96 | } 97 | 98 | /** 99 | * Add / to the url if not added 100 | * 101 | * @return string 102 | */ 103 | private function to(): string 104 | { 105 | $to = self::toUrl(); 106 | if ($to[strlen($to) - 1] !== '/') { 107 | $to .= '/'; 108 | } 109 | return $to; 110 | } 111 | 112 | /** 113 | * Prepares the response to be sent 114 | * 115 | * @param string $action 116 | * @return Response 117 | */ 118 | private function respond($action, $resp) 119 | { 120 | $this->responseStatusCode = $this->http()->getStatusCode(); 121 | 122 | $beforeMethod = 'before' . ucfirst($action) . 'Response'; 123 | $data = $resp; 124 | 125 | if ($action !== 'index') { 126 | if (!$this->http()->hasErrors()) { 127 | if (array_key_exists('data', $resp)) { 128 | $resp['data'] = (new Dud())->forceFill($resp['data']); 129 | $data =& $resp['data']; 130 | } else { 131 | $resp = (new Dud())->forceFill($resp); 132 | $data = $resp; 133 | } 134 | } else { 135 | $data = null; 136 | } 137 | } 138 | 139 | if ($data && $response = $this->$beforeMethod($data)) { 140 | return $response; 141 | } 142 | return response()->json($resp, $this->http()->getStatusCode()); 143 | } 144 | 145 | private function request($action, $id = null, $data = null) 146 | { 147 | $headerMethod = $action . 'Headers'; 148 | $options = [ 149 | 'query' => request()->query(), 150 | 'headers' => $this->$headerMethod() 151 | ]; 152 | if ($data) { 153 | $options['json'] = $data; 154 | } 155 | $method = $this->methodMap($action); 156 | $resp = $this->httpRequest($method, self::to() . $id, $options); 157 | return $this->respond($action, $resp); 158 | } 159 | 160 | /** 161 | * Shortcut for making http requests 162 | * 163 | * @param string $method GET|POST|PUT|PATCH|DELETE ... 164 | * @param string $url 165 | * @param array $options 166 | * @return mixed 167 | */ 168 | protected function httpRequest($method, $url, array $options = []) 169 | { 170 | return $this->http()->request($method, $url, $options); 171 | } 172 | 173 | /** 174 | * Shortcut to get the status code of the last request 175 | * 176 | * @return int 177 | */ 178 | protected function httpStatusCode(): int 179 | { 180 | return $this->http()->getStatusCode(); 181 | } 182 | 183 | /** 184 | * Shortcut to get the raw response object of the guzzle request 185 | * 186 | * @return mixed 187 | */ 188 | protected function httpResponse() 189 | { 190 | return $this->http()->rawResponse(); 191 | } 192 | 193 | /** 194 | * Check wither the request has errors or not 195 | * 196 | * @return bool 197 | */ 198 | protected function hasErrors(): bool 199 | { 200 | return $this->http()->hasErrors(); 201 | } 202 | 203 | /** 204 | * Display a listing of the resource. 205 | * @return Response 206 | */ 207 | public function index() 208 | { 209 | return $this->request('index'); 210 | } 211 | 212 | /** 213 | * Store a newly created resource in storage. 214 | * @param Request $request 215 | * @return Response 216 | */ 217 | public function store(Request $request) 218 | { 219 | $data = $this->validateRequest(); 220 | 221 | if ($resp = $this->beforeStore($data)) { 222 | return $resp; 223 | } 224 | 225 | return $this->request('store', null, $data); 226 | } 227 | 228 | /** 229 | * Fetch a resource 230 | * @param int $id 231 | * @return Response 232 | */ 233 | public function show($id) 234 | { 235 | return $this->request('show', $id); 236 | } 237 | 238 | /** 239 | * Update the specified resource in storage. 240 | * @param Request $request 241 | * @param int $id 242 | * @return Response 243 | */ 244 | public function update(Request $request, $id) 245 | { 246 | $requestData = $request->all(); 247 | $data = $this->validateRequest($this->validationRules($requestData, $id), $this->validationMessages($requestData, $id)); 248 | 249 | if ($resp = $this->beforeUpdate($data, (new Dud())->forceFill($data))) { 250 | return $resp; 251 | } 252 | 253 | return $this->request('update', $id, $data); 254 | } 255 | 256 | /** 257 | * Remove the specified resource from storage. 258 | * @param int $id 259 | * @return Response 260 | */ 261 | public function destroy($id) 262 | { 263 | return $this->request('destroy', $id); 264 | } 265 | 266 | /** 267 | * An array map of actions to request methods 268 | * 269 | * @param string $action The action for which map should be returned 270 | * @return array|string 271 | */ 272 | protected function methodMap($action = null) 273 | { 274 | $map = [ 275 | 'index' => 'GET', 276 | 'store' => 'POST', 277 | 'show' => 'GET', 278 | 'update' => 'PUT', 279 | 'destroy' => 'DELETE' 280 | ]; 281 | 282 | if ($action) { 283 | return $map[$action]; 284 | } 285 | return $map; 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /src/Controllers/Traits/Crud/Store.php: -------------------------------------------------------------------------------- 1 | storeModel(); 49 | } 50 | 51 | /** 52 | * Called after validation but before store method is called 53 | * 54 | * @param array $data 55 | * @return mixed The response to send or null 56 | */ 57 | protected function beforeStore(array &$data) {} 58 | 59 | /** 60 | * Called when an error occurs in a store operation 61 | * 62 | * @param array $data The data from the request 63 | * @param Model $model The created model 64 | * @return void 65 | */ 66 | protected function rollbackStore(array $data, Model $model) {} 67 | 68 | /** 69 | * Create 70 | * 71 | * Creates a new item. 72 | * 73 | * @param Request $request 74 | * @return Response 75 | */ 76 | public function store(Request $request) 77 | { 78 | $model = $this->storeModel(); 79 | 80 | if (method_exists($this, 'authorizeMethod')) { 81 | $this->authorizeMethod('store', [$model]); 82 | } 83 | 84 | $model = $this->storeModel(); 85 | 86 | if (!$model) { 87 | return $this->modelNotSetError('Store model undefined'); 88 | } 89 | 90 | $data = $this->validateRequest(); 91 | $item = null; 92 | 93 | return DB::transaction( 94 | function () use (&$data, $model, &$item) { 95 | if ($resp = $this->beforeStore($data)) { 96 | return $resp; 97 | } 98 | 99 | $item = is_object($model) 100 | ? $model->create($data) 101 | : $model::create($data); 102 | 103 | if (!$item) { 104 | throw new Exception('Create method returned falsable'); 105 | } 106 | 107 | if ($resp = $this->beforeStoreResponse($item)) { 108 | return $resp; 109 | } 110 | 111 | return $this->storeResponse($item); 112 | }, 113 | function ($ex) use ($data, $item) { 114 | $message = $ex->getMessage(); 115 | 116 | try { 117 | $this->rollbackStore($data, $item ?? new Dud()); 118 | } catch (Exception $ex) { 119 | $message = $ex->getMessage(); 120 | } 121 | 122 | return $this->storeFailedError($message); 123 | } 124 | ); 125 | } 126 | 127 | /** 128 | * Called on success but before sending the response 129 | * 130 | * @param Model $model 131 | * @return mixed The response to send or null 132 | */ 133 | protected function beforeStoreResponse(Model $model) {} 134 | 135 | /** 136 | * Called for the response to method store() 137 | * 138 | * @param Model $model 139 | * @return Response|array 140 | */ 141 | abstract protected function storeResponse(Model $model); 142 | 143 | /** 144 | * Called after validation but before store method is called 145 | * 146 | * @param array $data 147 | * @return mixed The response to send or null 148 | */ 149 | protected function beforeStoreMany(array &$data) {} 150 | 151 | /** 152 | * Called when an error occurs in a storeMany operation 153 | * 154 | * @param array $data The data from the request 155 | * @param array $models The created model 156 | * @return void 157 | */ 158 | protected function rollbackStoreMany(array $data, array $models) {} 159 | 160 | /** 161 | * Create (multiple) 162 | * 163 | * Creates multiple new items in one request. 164 | * 165 | * @param Request $request 166 | * @return Response 167 | */ 168 | public function storeMany(Request $request) 169 | { 170 | $model = $this->storeModel(); 171 | 172 | if (!$model) { 173 | return $this->modelNotSetError('Store model undefined'); 174 | } 175 | 176 | if (method_exists($this, 'authorizeMethod')) { 177 | $this->authorizeMethod('storeMany', [$model]); 178 | } 179 | 180 | $rules = $this->manyValidationRules($request->all()); 181 | $messages = $this->manyValidationMessages($request->all()); 182 | 183 | $ruleKeys = array_keys($rules); 184 | 185 | $data = $this->getManyValues($request->many, $ruleKeys); 186 | 187 | if ($resp = $this->validateRequest($rules, $messages)) { 188 | return $resp; 189 | } 190 | 191 | $items = []; 192 | 193 | return DB::transaction( 194 | function () use (&$data, $model, &$items) { 195 | if ($resp = $this->beforeStoreMany($data)) { 196 | return $resp; 197 | } 198 | 199 | foreach ($data as $currentData) { 200 | $item = is_object($model) 201 | ? $model->create($currentData) 202 | : $model::create($currentData); 203 | 204 | if (!$item) { 205 | throw new Exception('Create failed'); 206 | } 207 | 208 | $items[] = $item; 209 | } 210 | 211 | if ($resp = $this->beforeStoreManyResponse($items)) { 212 | return $resp; 213 | } 214 | 215 | return $this->storeManyResponse($items); 216 | }, 217 | function ($ex) use ($data, $items) { 218 | $message = $ex->getMessage(); 219 | 220 | try { 221 | $this->rollbackStoreMany($data, $items); 222 | } catch (Exception $ex) { 223 | $message = $ex->getMessage(); 224 | } 225 | 226 | return $this->storeFailedError($message); 227 | } 228 | ); 229 | } 230 | 231 | private function getManyValues($many, $ruleKeys) 232 | { 233 | $items = []; 234 | 235 | $keysString = str_replace('many.*.', '', join('/', $ruleKeys)); 236 | $ruleKeys = explode('/', $keysString); 237 | 238 | foreach ($many as $item) { 239 | $items[] = Arr::only($item, $ruleKeys); 240 | } 241 | 242 | return $items; 243 | } 244 | 245 | /** 246 | * Called on success but before sending the response 247 | * 248 | * @param array $data 249 | * @return mixed The response to send or null 250 | */ 251 | protected function beforeStoreManyResponse(array &$data) {} 252 | 253 | /** 254 | * Called for the response to method storeMany() 255 | * 256 | * @param array $data 257 | * @return Response|array 258 | */ 259 | abstract protected function storeManyResponse(array $data); 260 | } 261 | -------------------------------------------------------------------------------- /src/Controllers/Traits/Crud/Index.php: -------------------------------------------------------------------------------- 1 | [], 17 | 'fields' => [], 18 | 'filters' => [], 19 | 'includes' => [], 20 | 'sorts' => [], 21 | ]; 22 | 23 | /** 24 | * Create a model not set error response 25 | * 26 | * @return Response 27 | */ 28 | abstract protected function modelNotSetError($message = 'Model not set'); 29 | 30 | /** 31 | * The model to use in the index method. 32 | * 33 | * @return mixed 34 | */ 35 | abstract protected function indexModel(); 36 | 37 | /** 38 | * Sets the default pagination length 39 | * 40 | * @return integer 41 | */ 42 | protected function defaultPaginationLength(): int 43 | { 44 | return 15; 45 | } 46 | 47 | /** 48 | * Set allowed types 49 | * 50 | * @param string $type includes | filters | sorts | appends | fields 51 | * @param string|array $value 52 | * @return self 53 | */ 54 | protected function allowed($type, $value) 55 | { 56 | $this->allowed[$type] = is_array($value) ? join(',', $value) : $value; 57 | 58 | return $this; 59 | } 60 | 61 | /** 62 | * Set alowed appends 63 | * 64 | * @return array|string 65 | */ 66 | protected function allowedAppends() 67 | { 68 | return $this->allowed['appends'] ?? []; 69 | } 70 | 71 | /** 72 | * Set alowed fields 73 | * 74 | * @return array|string 75 | */ 76 | protected function allowedFields() 77 | { 78 | return $this->allowed['fields'] ?? []; 79 | } 80 | 81 | /** 82 | * Set allowed filters 83 | * 84 | * @return array|string 85 | */ 86 | protected function allowedFilters() 87 | { 88 | return $this->allowed['filters'] ?? []; 89 | } 90 | 91 | /** 92 | * Set allowed includes 93 | * 94 | * @return array|string 95 | */ 96 | protected function allowedIncludes() 97 | { 98 | return $this->allowed['includes'] ?? []; 99 | } 100 | 101 | /** 102 | * Set allowed sorts 103 | * 104 | * @return array|string 105 | */ 106 | protected function allowedSorts() 107 | { 108 | return $this->allowed['sorts'] ?? []; 109 | } 110 | 111 | /** 112 | * Set default sort 113 | * 114 | * @return string 115 | */ 116 | protected function defaultSort() 117 | { 118 | } 119 | 120 | private function isValid($param): bool 121 | { 122 | return $param && ((is_array($param) && count($param)) || is_string($param)); 123 | } 124 | 125 | private function build($model) 126 | { 127 | $createBuilder = function ($builder) use ($model) { 128 | if ($builder) { 129 | return $builder; 130 | } 131 | 132 | return QueryBuilder::for($model); 133 | }; 134 | 135 | $builder = null; 136 | 137 | if ($this->isValid($this->allowedAppends())) { 138 | $builder = $createBuilder($builder); 139 | $builder->allowedAppends($this->allowedAppends()); 140 | } 141 | 142 | if ($this->isValid($this->allowedFields())) { 143 | $builder = $createBuilder($builder); 144 | $builder->allowedFields($this->allowedFields()); 145 | } 146 | 147 | if ($this->isValid($this->allowedFilters())) { 148 | $builder = $createBuilder($builder); 149 | $builder->allowedFilters($this->allowedFilters()); 150 | } 151 | 152 | if ($this->isValid($this->allowedIncludes())) { 153 | $builder = $createBuilder($builder); 154 | $builder->allowedIncludes($this->allowedIncludes()); 155 | } 156 | 157 | if ($this->isValid($this->defaultSort())) { 158 | $builder = $createBuilder($builder); 159 | $builder->defaultSort($this->defaultSort()); 160 | } 161 | 162 | if ($this->isValid($this->allowedSorts())) { 163 | $builder = $createBuilder($builder); 164 | $builder->allowedSorts($this->allowedSorts()); 165 | } 166 | 167 | if (!$builder) { 168 | $builder = $model; 169 | } 170 | 171 | return $builder; 172 | } 173 | 174 | /** 175 | * List 176 | * 177 | * Get a paginated list of items 178 | * 179 | * @queryParam length number This is the number of items to return per page. If not provided, the default is used. Set as "-1" to return all items at once. Example: 15 180 | * @queryParam page number This is the current page to be items to return. Example: 1 181 | * 182 | * @return Response 183 | */ 184 | public function index() 185 | { 186 | $model = $this->indexModel(); 187 | 188 | if (!$model) { 189 | logger()->error('Index model undefined'); 190 | 191 | return $this->modelNotSetError(); 192 | } 193 | 194 | $length = (int) request('length', $this->defaultPaginationLength()); 195 | 196 | $model = $this->build($model); 197 | 198 | if ($length === -1) { 199 | $data = is_object($model) ? $model->get() : $model::all(); 200 | } else { 201 | $data = is_object($model) ? $model->paginate($length) : $model::paginate($length); 202 | } 203 | 204 | if ($resp = $this->beforeIndexResponse($data)) { 205 | return $resp; 206 | } 207 | 208 | return $this->indexResponse($data); 209 | } 210 | 211 | /** 212 | * Called before sending the response 213 | * 214 | * @param mixed $data 215 | * @return mixed The response to send or null 216 | */ 217 | protected function beforeIndexResponse(&$data) 218 | { 219 | } 220 | 221 | /** 222 | * Called for the response to method index() 223 | * 224 | * @param array|LengthAwarePaginator $data 225 | * @return Response|array 226 | */ 227 | abstract protected function indexResponse(LengthAwarePaginator | array $data); 228 | 229 | 230 | // ------------------ TRASHED INDEX --------------------- 231 | 232 | /** 233 | * 234 | * List (deleted) 235 | * 236 | * Get a list of items marked as deleted 237 | * 238 | * @queryParam length number This is the number of items to return per page. If not provided, the default is used. Set as "-1" to return all items at once. Example: 15 239 | * @queryParam page number This is the current page to be items to return. Example: 1 240 | * 241 | * @return Response 242 | */ 243 | public function trashedIndex() 244 | { 245 | $model = $this->indexModel(); 246 | 247 | if (!$model) { 248 | logger()->error('Index model undefined'); 249 | 250 | return $this->modelNotSetError(); 251 | } 252 | 253 | $length = (int) request('length', $this->defaultPaginationLength()); 254 | 255 | $model = $this->build($model); 256 | 257 | if ($length === -1) { 258 | $data = is_object($model) ? 259 | $model->onlyTrashed()->get() : 260 | $model::onlyTrashed()->all(); 261 | } else { 262 | $data = is_object($model) ? 263 | $model->onlyTrashed()->paginate($length) : 264 | $model::onlyTrashed()->paginate($length); 265 | } 266 | 267 | if ($resp = $this->beforeTrashedIndexResponse($data)) { 268 | return $resp; 269 | } 270 | 271 | return $this->trashedIndexResponse($data); 272 | } 273 | 274 | /** 275 | * Called before sending the response 276 | * 277 | * @param mixed $data 278 | * @return mixed The response to send or null 279 | */ 280 | protected function beforeTrashedIndexResponse(&$data) 281 | { 282 | } 283 | 284 | /** 285 | * Called for the response to method trashedIndex(). Defaults to @see indexResponse(). 286 | * 287 | * @param array $data 288 | * @return Response|array 289 | */ 290 | protected function trashedIndexResponse(LengthAwarePaginator | array $data) 291 | { 292 | return $this->indexResponse($data); 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /src/Tests/Traits/Api.php: -------------------------------------------------------------------------------- 1 | methods)) { 87 | throw new InvalidArgumentException('Unknown method [' . $method . ']'); 88 | } 89 | 90 | $url = $this->indexUrl(); 91 | 92 | if (!in_array($method, ['index', 'store'])) { 93 | $url .= '/' . $model->id; 94 | } 95 | 96 | return $url; 97 | } 98 | 99 | /** 100 | * Called before sending the index request 101 | * 102 | * @return void 103 | */ 104 | protected function beforeIndex(): void 105 | { 106 | $count = 0; 107 | $totalModels = $this->indexModelCount; 108 | 109 | while ($count < $totalModels) { 110 | $this->createModel(); 111 | $count++; 112 | } 113 | } 114 | 115 | /** 116 | * Called before sending the store request 117 | * 118 | * @param array $data 119 | * @return void 120 | */ 121 | protected function beforeStore(array &$data): void 122 | { 123 | } 124 | 125 | /** 126 | * Called before sending the update request 127 | * 128 | * @param array $data 129 | * @param Model $model 130 | * @return void 131 | */ 132 | protected function beforeUpdate(array &$data, Model $model): void 133 | { 134 | } 135 | 136 | /** 137 | * Called before calling the show request 138 | * 139 | * @param Model $model 140 | * @return void 141 | */ 142 | protected function beforeShow(Model $model): void 143 | { 144 | } 145 | 146 | /** 147 | * Called before callign the destroy request 148 | * 149 | * @param Model $model 150 | * @return void 151 | */ 152 | protected function beforeDestroy(Model $model): void 153 | { 154 | } 155 | 156 | /** 157 | * Transform a data to a response data 158 | * 159 | * @param array $data 160 | * @return array 161 | */ 162 | protected function makeResponse(array $data): array 163 | { 164 | return ['data' => $data]; 165 | } 166 | 167 | /** 168 | * Called to test index response 169 | * 170 | * @param TestResponse $response 171 | * @return void 172 | */ 173 | protected function assertIndex(TestResponse $response): void 174 | { 175 | $response->assertStatus(Response::HTTP_OK); 176 | } 177 | 178 | /** 179 | * Called to test store response 180 | * 181 | * @param TestResponse $response 182 | * @param array $payload 183 | * @return void 184 | */ 185 | protected function assertStore(TestResponse $response, array $payload): void 186 | { 187 | $response->assertStatus(Response::HTTP_CREATED) 188 | ->assertJson($this->makeResponse($payload)); 189 | } 190 | 191 | /** 192 | * Called to test update response 193 | * 194 | * @param TestResponse $response 195 | * @param Model $model 196 | * @param array $payload 197 | * @return void 198 | */ 199 | protected function assertUpdate(TestResponse $response, Model $model, array $payload): void 200 | { 201 | $model->refresh(); 202 | 203 | $response->assertStatus(Response::HTTP_ACCEPTED) 204 | ->assertJson($this->makeResponse($payload)); 205 | } 206 | 207 | /** 208 | * Called to test show response 209 | * 210 | * @param TestResponse $response 211 | * @return void 212 | */ 213 | protected function assertShow(TestResponse $response): void 214 | { 215 | $response->assertStatus(Response::HTTP_OK); 216 | } 217 | 218 | /** 219 | * Called to test destroy response 220 | * 221 | * @param TestResponse $response 222 | * @param Model $model 223 | * @return void 224 | */ 225 | protected function assertDestroy(TestResponse $response, Model $model): void 226 | { 227 | $response->assertStatus(Response::HTTP_ACCEPTED); 228 | 229 | $model = $model->fresh(); 230 | 231 | if ($model && method_exists($model, 'trashed')) { 232 | $this->assertTrue($model->trashed(), 'Model not soft-deleted'); 233 | } else { 234 | $this->assertNull($model, 'Model not deleted'); 235 | } 236 | } 237 | 238 | public function testStore() 239 | { 240 | if (!$this->canTest('store')) { 241 | return $this->markTestSkipped(); 242 | } 243 | 244 | $payload = $this->payload(); 245 | $this->beforeStore($payload); 246 | 247 | $response = $this->post($this->createUrl('store', null, $payload), $payload); 248 | 249 | if ($this->storeResponses) { 250 | $this->storeResponse($response, $this->storePaths['store'] ?? $this->resource() . '/store'); 251 | } 252 | 253 | $this->assertStore($response, $payload); 254 | } 255 | 256 | public function testIndex() 257 | { 258 | if (!$this->canTest('index')) { 259 | return $this->markTestSkipped(); 260 | } 261 | 262 | $this->beforeIndex(); 263 | $response = $this->get($this->createUrl('index')); 264 | 265 | if ($this->storeResponses) { 266 | $this->storeResponse($response, $this->storePaths['index'] ?? $this->resource() . '/index'); 267 | } 268 | 269 | $this->assertIndex($response); 270 | } 271 | 272 | public function testShow() 273 | { 274 | if (!$this->canTest('show')) { 275 | return $this->markTestSkipped(); 276 | } 277 | 278 | $model = $this->createModel(); 279 | $this->beforeShow($model); 280 | 281 | $response = $this->get($this->createUrl('show', $model)); 282 | 283 | if ($this->storeResponses) { 284 | $this->storeResponse($response, $this->storePaths['show'] ?? $this->resource() . '/show'); 285 | } 286 | 287 | $this->assertShow($response); 288 | } 289 | 290 | public function testUpdate() 291 | { 292 | if (!$this->canTest('update')) { 293 | return $this->markTestSkipped(); 294 | } 295 | 296 | $payload = $this->payload(true); 297 | $model = $this->createModel(); 298 | 299 | $this->beforeUpdate($payload, $model); 300 | 301 | $response = $this->put($this->createUrl('update', $model, $payload), $payload); 302 | 303 | if ($this->storeResponses) { 304 | $this->storeResponse($response, $this->storePaths['update'] ?? $this->resource() . '/update'); 305 | } 306 | 307 | $this->assertUpdate($response, $model, $payload); 308 | } 309 | 310 | public function testDestroy() 311 | { 312 | if (!$this->canTest('destroy')) { 313 | return $this->markTestSkipped(); 314 | } 315 | 316 | $model = $this->createModel(); 317 | $this->beforeDestroy($model); 318 | 319 | $response = $this->delete($this->createUrl('destroy', $model)); 320 | 321 | if ($this->storeResponses) { 322 | $this->storeResponse($response, $this->storePaths['destroy'] ?? $this->resource() . '/destroy'); 323 | } 324 | 325 | $this->assertDestroy($response, $model); 326 | } 327 | 328 | private function canTest($method): bool 329 | { 330 | return in_array($method, $this->methods); 331 | } 332 | 333 | private function resource() 334 | { 335 | if (!$this->resource) { 336 | $filename = basename(str_replace('\\', '/', get_called_class())); 337 | $filename = Str::beforeLast($filename, 'Test'); 338 | $this->resource = (Str::kebab($filename)); 339 | } 340 | 341 | return $this->resource; 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /src/Controllers/Traits/Crud/Destroy.php: -------------------------------------------------------------------------------- 1 | destroyModel(); 85 | 86 | if (!$model) { 87 | return $this->modelNotSetError('Destroy model undefined'); 88 | } 89 | 90 | $item = $this->find($model, $id); 91 | 92 | if (!$item) { 93 | return $this->notFoundError(); 94 | } 95 | 96 | if (method_exists($this, 'authorizeMethod')) { 97 | $this->authorizeMethod('destroy', [$model, $item]); 98 | } 99 | 100 | return DB::transaction( 101 | function () use (&$item) { 102 | if ($resp = $this->beforeDestroy($item)) { 103 | return $resp; 104 | } 105 | 106 | $result = $item->delete(); 107 | 108 | if (!$result) { 109 | throw new Exception('Delete method returned falsable'); 110 | } 111 | 112 | if ($resp = $this->beforeDestroyResponse($item)) { 113 | return $resp; 114 | } 115 | 116 | return $this->destroyResponse($item); 117 | }, 118 | function ($ex) use ($item) { 119 | $message = $ex->getMessage(); 120 | 121 | try { 122 | $this->rollbackDestroy($item); 123 | } catch (Exception $ex) { 124 | $message = $ex->getMessage(); 125 | } 126 | 127 | return $this->destroyFailedError($message); 128 | } 129 | ); 130 | } 131 | 132 | /** 133 | * Called on success but before sending the response 134 | * 135 | * @param Model $model 136 | * @return mixed The response to send or null 137 | */ 138 | protected function beforeDestroyResponse(Model $model) {} 139 | 140 | /** 141 | * Called for the response to method @see destroy() 142 | * 143 | * @param Model $model 144 | * @return Response|array 145 | */ 146 | abstract protected function destroyResponse(Model $model); 147 | 148 | // ------------------ DESTROY MANY --------------------------------- 149 | 150 | /** 151 | * Called when the model has been found but before deleting 152 | * 153 | * @param array $data 154 | * @return void 155 | */ 156 | protected function beforeDestroyMany(array &$data) {} 157 | 158 | /** 159 | * Called when an error occurs in a delete operation 160 | * @param array $ids The id of the models to be deleted 161 | * @return void 162 | */ 163 | protected function rollbackDestroyMany(array $ids) {} 164 | /** 165 | * Delete many 166 | * 167 | * Deletes the specified items. 168 | * 169 | * @bodyParams ids string[]|integer[] required Array of ids of items to delete. 170 | * 171 | * @return Response 172 | */ 173 | public function destroyMany(Request $request) 174 | { 175 | $model = $this->destroyModel(); 176 | 177 | if (!$model) { 178 | return $this->modelNotSetError('Destroy model undefined'); 179 | } 180 | 181 | if (method_exists($this, 'authorizeMethod')) { 182 | $this->authorizeMethod('destroyMany', [$model]); 183 | } 184 | 185 | $data = $request->all(); 186 | 187 | if (!array_key_exists('ids', $data)) { 188 | throw new Exception('Ids not found'); 189 | } 190 | 191 | return DB::transaction( 192 | function () use (&$data, $model) { 193 | if ($resp = $this->beforeDestroyMany($data)) { 194 | return $resp; 195 | } 196 | 197 | $result = is_object($model) 198 | ? $model->whereIn('id', $data['ids'])->delete() 199 | : $model::whereIn('id', $data['ids'])->delete(); 200 | 201 | if (!$result) { 202 | throw new Exception('Delete failed'); 203 | } 204 | 205 | if ($resp = $this->beforeDestroyManyResponse($result, $data['ids'])) { 206 | return $resp; 207 | } 208 | 209 | return $this->destroyManyResponse($result); 210 | }, 211 | function ($ex) use ($data) { 212 | $message = $ex->getMessage(); 213 | 214 | try { 215 | $this->rollbackDestroyMany($data['ids']); 216 | } catch (\Exception $ex) { 217 | $message = $ex->getMessage(); 218 | } 219 | 220 | return $this->destroyFailedError($message); 221 | } 222 | ); 223 | } 224 | 225 | /** 226 | * Called on success but before sending the response 227 | * 228 | * @param integer $deleteCount 229 | * @return mixed The response to send or null 230 | */ 231 | protected function beforeDestroyManyResponse($deletedCount) {} 232 | 233 | /** 234 | * Called for the response to method destroyMany() 235 | * 236 | * @param integer $deletedCount 237 | * @return Response|array 238 | */ 239 | abstract protected function destroyManyResponse($deleteCount); 240 | 241 | // -------------------- FORCE DESTROY ------------------------------ 242 | 243 | /** 244 | * Called when the model has been found but before force deleting 245 | * 246 | * @param Model $model 247 | * @return void 248 | */ 249 | protected function beforeForceDestroy(Model $model) {} 250 | 251 | /** 252 | * Called when an error occurs in a force delete operation 253 | * 254 | * @param Model $model 255 | * @return void 256 | */ 257 | protected function rollbackForceDestroy(Model $model) {} 258 | 259 | /** 260 | * Delete (permanently) 261 | * 262 | * Permanently deletes the specified item. 263 | * 264 | * @urlParam id integer|string required The id of the item to delete. 265 | * 266 | * @param int $id 267 | * @return Response 268 | */ 269 | public function forceDestroy($id) 270 | { 271 | $model = $this->destroyModel(); 272 | 273 | if (!$model) { 274 | return $this->modelNotSetError('Destroy model undefined'); 275 | } 276 | 277 | $item = $this->find($model, $id); 278 | 279 | if (!$item) { 280 | return $this->notFoundError(); 281 | } 282 | 283 | if (method_exists($this, 'authorizeMethod')) { 284 | $this->authorizeMethod('forceDestroy', [$model, $item]); 285 | } 286 | 287 | return DB::transaction( 288 | function () use (&$item) { 289 | if ($resp = $this->beforeForceDestroy($item)) { 290 | return $resp; 291 | } 292 | 293 | $result = $item->forceDelete(); 294 | 295 | if (!$result) { 296 | throw new Exception('Force delete failed'); 297 | } 298 | 299 | if ($resp = $this->beforeForceDestroyResponse($item)) { 300 | return $resp; 301 | } 302 | 303 | return $this->forceDestroyResponse($item); 304 | }, 305 | function ($ex) use ($item) { 306 | $message = $ex->getMessage(); 307 | 308 | try { 309 | $this->rollbackForceDestroy($item); 310 | } catch (Exception $ex) { 311 | $message = $ex->getMessage(); 312 | } 313 | 314 | return $this->destroyFailedError($message); 315 | } 316 | ); 317 | } 318 | 319 | /** 320 | * Called on success but before sending the response 321 | * 322 | * @param mixed $model 323 | * @return mixed The response to send or null 324 | */ 325 | protected function beforeForceDestroyResponse(Model $model) {} 326 | 327 | /** 328 | * Called for the response to method @see forceDestroy() 329 | * 330 | * @param Model $model 331 | * @return Response|array 332 | */ 333 | protected function forceDestroyResponse(Model $model) 334 | { 335 | return $this->destroyResponse($model); 336 | } 337 | 338 | // ---------------- RESTORE DESTROYED ------------------------- 339 | 340 | /** 341 | * Called when the model has been found but before restoring a deleted resource 342 | * 343 | * @param Model $model 344 | * @return void 345 | */ 346 | protected function beforeRestoreDestroyed(Model $model) {} 347 | 348 | /** 349 | * Called when an error occurs in a restore destroyed operation 350 | * 351 | * @param Model $model 352 | * @return void 353 | */ 354 | protected function rollbackRestoreDestroyed(Model $model) {} 355 | 356 | /** 357 | * Restore 358 | * 359 | * Undeletes the specified item which was previously marked as deleted. 360 | * 361 | * @urlParam id integer|string required The id of the item to undelete. 362 | * 363 | * @param int $id 364 | * @return Response 365 | */ 366 | public function restoreDestroyed($id) 367 | { 368 | $model = $this->destroyModel(); 369 | 370 | if (!$model) { 371 | return $this->modelNotSetError('Destroy model undefined'); 372 | } 373 | 374 | $item = $this->find($model, $id); 375 | 376 | if (!$item) { 377 | return $this->notFoundError(); 378 | } 379 | 380 | return DB::transaction( 381 | function () use (&$item) { 382 | if ($resp = $this->beforeRestoreDestroyed($item)) { 383 | return $resp; 384 | } 385 | 386 | $result = $item->restore(); 387 | 388 | if (!$result) { 389 | throw new Exception('Restore failed'); 390 | } 391 | 392 | if ($resp = $this->beforeRestoreDestroyedResponse($item)) { 393 | return $resp; 394 | } 395 | 396 | return $this->restoreDestroyedResponse($item); 397 | }, 398 | function ($ex) use ($item) { 399 | $message = $ex->getMessage(); 400 | 401 | try { 402 | $this->rollbackRestoreDestroyed($item); 403 | } catch (Exception $ex) { 404 | $message = $ex->getMessage(); 405 | } 406 | 407 | return $this->restoreFailedError($message); 408 | } 409 | ); 410 | } 411 | 412 | /** 413 | * Called on success but before sending the response 414 | * 415 | * @param Model $model 416 | * @return mixed The response to send or null 417 | */ 418 | protected function beforeRestoreDestroyedResponse(Model $model) {} 419 | 420 | /** 421 | * Called for the response to method @see restoreDestroyed() 422 | * 423 | * @param Model $model 424 | * @return Response|array 425 | */ 426 | abstract protected function restoreDestroyedResponse(Model $model); 427 | } 428 | --------------------------------------------------------------------------------