diff --git a/app/Http/API/V1/PersonalCabinetController.php b/app/Http/API/V1/PersonalCabinetController.php new file mode 100644 index 000000000..d1145432c --- /dev/null +++ b/app/Http/API/V1/PersonalCabinetController.php @@ -0,0 +1,98 @@ +load('profile'); + + return response()->json([ + 'user' => $user, + 'profile' => $user->profile, + ]); + } + + /** + * Обновить профиль пользователя + * + * @param Request $request + * @param User $user Авторизованный пользователь + * @return \Illuminate\Http\JsonResponse + */ + public function update(Request $request, Authenticatable $user) + { + $validator = Validator::make($request->all(), [ + 'name' => 'sometimes|string|max:255', + 'email' => 'sometimes|string|email|max:255|unique:users,email,' . $user->id, + 'phone' => 'nullable|string|max:20', + 'address' => 'nullable|string|max:255', + 'birth_date' => 'nullable|date', + 'about' => 'nullable|string', + ]); + + if ($validator->fails()) { + return response()->json($validator->errors(), 422); + } + + // Обновляем основные данные пользователя + if ($request->has('name')) { + $user->name = $request->name; + } + + if ($request->has('email')) { + $user->email = $request->email; + } + + $user->save(); + + // Обновляем или создаем профиль + $profileData = $request->only(['phone', 'address', 'birth_date', 'about']); + + if ($user->profile) { + $user->profile->update($profileData); + } else { + $profileData['user_id'] = $user->id; + UserProfile::create($profileData); + } + + return response()->json([ + 'message' => 'Profile updated successfully', + 'user' => $user->load('profile'), + ]); + } + + /** + * Удалить аккаунт пользователя + * + * @param User $user Авторизованный пользователь + * @return \Illuminate\Http\JsonResponse + */ + public function destroy() + { + // Получаем аутентифицированного пользователя + $user = auth('jwt')->user(); + + if ($user) { + $user->delete(); + } + + return response()->json([ + 'message' => 'Account deleted successfully', + ]); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 91f25d431..f9525b0a6 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -118,4 +118,9 @@ public function getJWTCustomClaims(): array 'name' => $this->name, ]; } + + public function profile() + { + return $this->hasOne(UserProfile::class); + } } diff --git a/app/Models/UserProfile.php b/app/Models/UserProfile.php new file mode 100644 index 000000000..be2efab2e --- /dev/null +++ b/app/Models/UserProfile.php @@ -0,0 +1,52 @@ +belongsTo(User::class); + } +} diff --git a/client.http b/client.http index 67eeccc57..79905b34d 100644 --- a/client.http +++ b/client.http @@ -1,5 +1,5 @@ @host = http://localhost -@token = eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0L2FwaS9hdXRoL3JlZnJlc2gtdG9rZW4iLCJpYXQiOjE3NTUwNjY1NDAsImV4cCI6MTc1NTA2NzQ0MCwibmJmIjoxNzU1MDY2NTQwLCJqdGkiOiJRdzYxSUFlaUp3b0tIY1E5Iiwic3ViIjoiMSIsInBydiI6IjIzYmQ1Yzg5NDlmNjAwYWRiMzllNzAxYzQwMDg3MmRiN2E1OTc2ZjciLCJyb2xlcyI6WzFdLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwibmFtZSI6IkFkbWluIFVzZXIifQ.AVplZoXXVmLGlapJMY17RNnUcqw5PH0vxtd1vu0yWWIcYM5WAFQ8sgFlrMH0sqaafMGTkikBTfyLGow6M2JxyS2xiCveMN6vxaBA_XwNUqASL4pIKDhkplB7rfxZO_vUxhMdT_0Qup6UQzAhRohq8-EWT6Ha9AjyOGXYSxWhAL1tGAUNLlAlk3Scsh5Zw4rkJRDhOx-OXPZFN3QxKO011Rc2uCzpeNS0j7Ij8zYw3KYO8ypLbPsajV6P1fIAso-Sn1rJ4jmITQnpHVPCPUkNh2O4Yc19KLOVx3W75PD59uaRCCod9mIkspBRJhlHwtlBi9p3q-9Q3Y3ePCbbhKLkVnEiWQj5sZ8L1z4pTi_syULBp6Fs9YCIGXZ8NbSXZUuyKjWCIw1w4XpUvM7W0RR8UdxNW8nzAWEaeE2WoPhGe7ZI0nqZte8oZUP2xCijCbQ8pqchHgAmuV9GsJjnGPN78kpv-xwAaJZpLxzXRRtM1lVYHUexsTiz6OVX67xrPTP4NCV7E9uy7Oed3FGWl6_bDSAE5gqw8YXd6hFrkZrgCHBoDYmbMcAtRcoFJSS6u6nkLpmN9N-_s_E2tgJQGwSeOtvEaACMkD-zO8pQzCGXX2mn8iApeNKd7FECLMwNuFfIqvdi_PrmZoykRLAqQI6pnFB6lGwL4mYuQBD7OjqlvyM +@token = eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0L2FwaS9hdXRoL2xvZ2luIiwiaWF0IjoxNzU1NDUzODkwLCJleHAiOjE3NTU0NTQ3OTAsIm5iZiI6MTc1NTQ1Mzg5MCwianRpIjoiNm9zblpRR29ENzVqYmFSWiIsInN1YiI6IjEiLCJwcnYiOiIyM2JkNWM4OTQ5ZjYwMGFkYjM5ZTcwMWM0MDA4NzJkYjdhNTk3NmY3Iiwicm9sZXMiOlsxXSwiZW1haWwiOiJhZG1pbkBleGFtcGxlLmNvbSIsIm5hbWUiOiJBZG1pbiBVc2VyIn0.r_AG11yeREiNHsFzw1w6er4TiVS5Ec4X-d-fpc3-Nd59UPHwQvc_OGpfNNs-Xh5RDqciEOms91m2Cu5h49xazYKRHHr5PERTxAUcZErf5qvhw08r9_1oouNrL5p97ABqkTurMGP9LcntPxM2P3kMML_X2O3u83RycqDzj_o5czCf8RNaTZeF-Rx6yAJf9DEEdNcsApxlF5-TXCBlNN8wG_tAb7sTT4LzSreAkt7SvjQaE7ZuBFP9Bdnx9SWWb1H-Vud1xD8wI89iKpkjl0vxufDlLxZhbZKTiUqp2T-QIDTlf6WAepfpSPMIjO7DgyQYAs9_CWURtc8lEYe5Xlo4RxOr0AI-in3DRDDH9wJAK8B4qpDo_9UXG4bsBlGlmhD3Vf81mteVYNx6ylukoqcZ7GpyaCO2JKEgxtEFZBRZ6SyvMVnp8SHRm53cluAeMpOSTLGo2XXr2cOFIZXoeWncWr8TU7RUTZttkUzUx1ZVdwzhMO53fAseHE7ZeiYjKxMw08rNckbOd-beVG87NF6GuKK13tAQzY5d_870WMU9byChw8UtXO_XE9P5nQIYO7oA7IAJKG8B-2022LroB7VNU9ssIZQ7vza1Q5plYFZdC0zgWFd6v-Ux359jN848fMWB2LGQs1CZG2WXN1EWRQReEl8TUUvc314t9XCiKObtTn8 ### User login POST {{host}}/api/auth/login @@ -20,6 +20,12 @@ Content-Type: application/json "refresh_token": "QzRYhsjlnlFuEWjKTqicvVufJplCneM9ddDppPkSsE5co1Kcote785iiKIlBwlWm" } +### User profile +GET {{host}}/api/v1/personal-cabinet +Accept: application/json +Content-Type: application/json +Authorization: Bearer {{token}} + ### Get all products (paginated) GET {{host}}/api/v1/products Authorization: Bearer {{token}} diff --git a/coverage-report/Console/Commands/ExportProductExcel.php.html b/coverage-report/Console/Commands/ExportProductExcel.php.html new file mode 100644 index 000000000..6b6ca5347 --- /dev/null +++ b/coverage-report/Console/Commands/ExportProductExcel.php.html @@ -0,0 +1,268 @@ + + +
+ +| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 3.33% |
+ 1 / 30 |
+
+
+
+ |
+ 50.00% |
+ 1 / 2 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| ExportProductExcel | +
+
+
+ |
+ 3.33% |
+ 1 / 30 |
+
+
+
+ |
+ 50.00% |
+ 1 / 2 |
+ 51.26 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| __construct | +
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 1 | ++ | ||
| handle | +
+
+
+ |
+ 0.00% |
+ 0 / 29 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 42 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Console\Commands; |
| 4 | |
| 5 | use Illuminate\Console\Command; |
| 6 | use Illuminate\Support\Facades\Storage; |
| 7 | use Ivan\ExportExcelProducts\Services\ExportExcel; |
| 8 | use Maatwebsite\Excel\Facades\Excel; |
| 9 | |
| 10 | class ExportProductExcel extends Command |
| 11 | { |
| 12 | /** |
| 13 | * The name and signature of the console command. |
| 14 | * |
| 15 | * @var string |
| 16 | */ |
| 17 | protected $signature = 'app:export-product-excel |
| 18 | {mode : type of export 1(group category) or 2 (only product)} |
| 19 | {--H|headers=* : list products fields export} |
| 20 | {--C|categories=* : list categories ids export} |
| 21 | '; |
| 22 | |
| 23 | /** |
| 24 | * The console command description. |
| 25 | * |
| 26 | * @var string |
| 27 | */ |
| 28 | protected $description = 'Export products to excel file'; |
| 29 | |
| 30 | |
| 31 | public function __construct() |
| 32 | { |
| 33 | parent::__construct(); |
| 34 | } |
| 35 | |
| 36 | /** |
| 37 | * Execute the console command. |
| 38 | */ |
| 39 | public function handle(ExportExcel $excel) |
| 40 | { |
| 41 | $mode = (int) $this->argument('mode'); |
| 42 | if (!in_array($mode, [1,2])) { |
| 43 | $this->error('Invalid mode. Please use 1 for grouped by category or 2 for only products.'); |
| 44 | return self::FAILURE; |
| 45 | } |
| 46 | |
| 47 | $availableColumns = [ |
| 48 | 'id' => 'ID', 'title' => 'Название', 'alias' => 'Псевдоним', 'text' => 'Описание', |
| 49 | 'image' => 'Изображение', 'is_sale' => 'На Распродаже', 'published' => 'Опубликовано', |
| 50 | 'order' => 'Порядок', 'price' => 'Цена', 'user_id' => 'ID Пользователя', |
| 51 | 'created_at' => 'Создано', 'updated_at' => 'Обновлено', |
| 52 | ]; |
| 53 | |
| 54 | $headers = $this->option('headers'); |
| 55 | if (empty($headers)) { |
| 56 | $headers = config('export_excel_products.default_column', ['id', 'title', 'price']); |
| 57 | } |
| 58 | |
| 59 | // Validate headers |
| 60 | $invalidHeaders = array_diff($headers, array_keys($availableColumns)); |
| 61 | if (!empty($invalidHeaders)) { |
| 62 | $this->warn('The following headers are invalid and will be ignored: ' . implode(', ', $invalidHeaders)); |
| 63 | $headers = array_intersect($headers, array_keys($availableColumns)); |
| 64 | } |
| 65 | |
| 66 | // Check if any valid headers are left |
| 67 | if(empty($headers)){ |
| 68 | $this->error('No valid headers were provided for export.'); |
| 69 | return self::FAILURE; |
| 70 | } |
| 71 | |
| 72 | $this->info('Starting product export...'); |
| 73 | |
| 74 | $this->table($headers, []); |
| 75 | |
| 76 | $categories = $this->option('categories'); |
| 77 | |
| 78 | $fileName = 'public/exports/products_export_' . date('Y_m_d_H_i_s') . '.xlsx'; |
| 79 | |
| 80 | //$path = Storage::disk('public')->path($fileName); |
| 81 | |
| 82 | if($mode == 2){ |
| 83 | $excel->saveProductsToFile($headers, $categories, $fileName); |
| 84 | }else{ |
| 85 | $excel->saveProductsGroupByCategoriesToFile($headers, $categories, $fileName); |
| 86 | } |
| 87 | |
| 88 | $this->info('Products saved to excel file successfully at: ' . $fileName); |
| 89 | |
| 90 | return self::SUCCESS; |
| 91 | |
| 92 | } |
| 93 | } |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 5.26% |
+ 1 / 19 |
+
+
+
+ |
+ 33.33% |
+ 1 / 3 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| WarmUpCache | +
+
+
+ |
+ 5.26% |
+ 1 / 19 |
+
+
+
+ |
+ 33.33% |
+ 1 / 3 |
+ 26.26 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| __construct | +
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 1 | ++ | ||
| handle | +
+
+
+ |
+ 0.00% |
+ 0 / 16 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 12 | ++ | ||
| cacheSupportsTags | +
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Console\Commands; |
| 4 | |
| 5 | use App\Repositories\Contracts\ProductRepositoryInterface; |
| 6 | use Illuminate\Console\Command; |
| 7 | use Illuminate\Support\Facades\Cache; |
| 8 | |
| 9 | class WarmUpCache extends Command |
| 10 | { |
| 11 | /** |
| 12 | * The name and signature of the console command. |
| 13 | * |
| 14 | * @var string |
| 15 | */ |
| 16 | protected $signature = 'cache:warm-products'; |
| 17 | |
| 18 | /** |
| 19 | * The console command description. |
| 20 | * |
| 21 | * @var string |
| 22 | */ |
| 23 | protected $description = 'Command description'; |
| 24 | |
| 25 | public function __construct(protected ProductRepositoryInterface $productRepository) |
| 26 | { |
| 27 | parent::__construct(); |
| 28 | } |
| 29 | |
| 30 | /** |
| 31 | * Execute the console command. |
| 32 | */ |
| 33 | public function handle() |
| 34 | { |
| 35 | $this->info('Starting cache warming...'); |
| 36 | |
| 37 | |
| 38 | $isSupportTags = $this->cacheSupportsTags(); |
| 39 | |
| 40 | $perPage = 10; |
| 41 | $totalProducts = $this->productRepository->count(); |
| 42 | |
| 43 | $totalPages = ceil($totalProducts / $perPage); |
| 44 | |
| 45 | |
| 46 | for ($page = 1; $page <= $totalPages; $page++) { |
| 47 | |
| 48 | $cacheKey = 'products_admin_page_' . $page . '_per_page_' . $perPage; |
| 49 | |
| 50 | if($isSupportTags){ |
| 51 | Cache::tags(['products'])->remember($cacheKey, 60, function () use ($page, $perPage) { |
| 52 | return $this->productRepository->getAllPaginated($perPage, 'order', $page); |
| 53 | }); |
| 54 | }else{ |
| 55 | Cache::remember($cacheKey, 60 * 2, function () use ($perPage, $page) { |
| 56 | return $this->productRepository->getAllPaginated($perPage, 'order', $page); |
| 57 | }); |
| 58 | } |
| 59 | |
| 60 | $this->info("Cached page {$page}/{$totalPages}"); |
| 61 | } |
| 62 | |
| 63 | $this->info('Products cache warmed successfully!'); |
| 64 | |
| 65 | |
| 66 | } |
| 67 | |
| 68 | /** |
| 69 | * Check if the current cache driver supports tags. |
| 70 | * |
| 71 | * @return bool |
| 72 | */ |
| 73 | protected function cacheSupportsTags(): bool |
| 74 | { |
| 75 | $driver = config('cache.default'); |
| 76 | return in_array($driver, ['redis', 'memcached']); |
| 77 | } |
| 78 | } |
| Class | +Coverage | +
|---|---|
| App\Console\Commands\ExportProductExcel | 3% |
| App\Console\Commands\WarmUpCache | 5% |
| Class | +CRAP | +
|---|---|
| App\Console\Commands\ExportProductExcel | 51 |
| App\Console\Commands\WarmUpCache | 26 |
| Method | +Coverage | +
|---|---|
| handle | 0% |
| handle | 0% |
| cacheSupportsTags | 0% |
| + | Code Coverage |
+ ||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ ||||||
| Total | +
+
+
+ |
+ 4.08% |
+ 2 / 49 |
+
+
+
+ |
+ 40.00% |
+ 2 / 5 |
+
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+
|
+
+
+ |
+ 3.33% |
+ 1 / 30 |
+
+
+
+ |
+ 50.00% |
+ 1 / 2 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
|
+
+
+ |
+ 5.26% |
+ 1 / 19 |
+
+
+
+ |
+ 33.33% |
+ 1 / 3 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
| Class | +Coverage | +
|---|---|
| App\Console\Commands\ExportProductExcel | 3% |
| App\Console\Commands\WarmUpCache | 5% |
| Class | +CRAP | +
|---|---|
| App\Console\Commands\ExportProductExcel | 51 |
| App\Console\Commands\WarmUpCache | 26 |
| Method | +Coverage | +
|---|---|
| handle | 0% |
| handle | 0% |
| cacheSupportsTags | 0% |
| + | Code Coverage |
+ ||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ ||||||
| Total | +
+
+
+ |
+ 4.08% |
+ 2 / 49 |
+
+
+
+ |
+ 40.00% |
+ 2 / 5 |
+
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+
|
+
+
+ |
+ 4.08% |
+ 2 / 49 |
+
+
+
+ |
+ 40.00% |
+ 2 / 5 |
+
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+ |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| ProductPriceData | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| __construct | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\DTO; |
| 4 | readonly class ProductPriceData |
| 5 | { |
| 6 | /** |
| 7 | * @param int $productId The ID of the product. |
| 8 | * @param float $oldPrice The old price of the product. |
| 9 | * @param float $newPrice The new price of the product. |
| 10 | */ |
| 11 | public function __construct( |
| 12 | public int $productId, |
| 13 | public float $oldPrice, |
| 14 | public float $newPrice |
| 15 | ) {} |
| 16 | } |
| Class | +Coverage | +
|---|---|
| App\DTO\ProductPriceData | 0% |
| Class | +CRAP | +
|---|
| Method | +Coverage | +
|---|---|
| __construct | 0% |
| Method | +CRAP | +
|---|
| + | Code Coverage |
+ ||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ ||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
|
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| ProductPriceChanged | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| __construct | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Events; |
| 4 | |
| 5 | use App\DTO\ProductPriceData; |
| 6 | use App\Models\Product; |
| 7 | use Illuminate\Broadcasting\InteractsWithSockets; |
| 8 | use Illuminate\Foundation\Events\Dispatchable; |
| 9 | use Illuminate\Queue\SerializesModels; |
| 10 | |
| 11 | class ProductPriceChanged |
| 12 | { |
| 13 | use Dispatchable, InteractsWithSockets, SerializesModels; |
| 14 | |
| 15 | public ProductPriceData $productPriceData; |
| 16 | |
| 17 | public function __construct(ProductPriceData $priceData) |
| 18 | { |
| 19 | $this->productPriceData = $priceData; |
| 20 | } |
| 21 | } |
| Class | +Coverage | +
|---|---|
| App\Events\ProductPriceChanged | 0% |
| Class | +CRAP | +
|---|
| Method | +Coverage | +
|---|---|
| __construct | 0% |
| Method | +CRAP | +
|---|
| + | Code Coverage |
+ ||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ ||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
|
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 100.00% |
+ 46 / 46 |
+
+
+
+ |
+ 100.00% |
+ 9 / 9 |
+ CRAP | +
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+
| AuthController | +
+
+
+ |
+ 100.00% |
+ 46 / 46 |
+
+
+
+ |
+ 100.00% |
+ 9 / 9 |
+ 11 | +
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+
| middleware | +
+
+
+ |
+ 100.00% |
+ 3 / 3 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 1 | ++ | ||
| login | +
+
+
+ |
+ 100.00% |
+ 6 / 6 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 2 | ++ | ||
| me | +
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 1 | ++ | ||
| logout | +
+
+
+ |
+ 100.00% |
+ 2 / 2 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 1 | ++ | ||
| refreshToken | +
+
+
+ |
+ 100.00% |
+ 13 / 13 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 2 | ++ | ||
| refresh | +
+
+
+ |
+ 100.00% |
+ 2 / 2 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 1 | ++ | ||
| createRefreshToken | +
+
+
+ |
+ 100.00% |
+ 7 / 7 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 1 | ++ | ||
| respondWithTokens | +
+
+
+ |
+ 100.00% |
+ 7 / 7 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 1 | ++ | ||
| respondWithToken | +
+
+
+ |
+ 100.00% |
+ 5 / 5 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 1 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Http\API\V1; |
| 4 | |
| 5 | |
| 6 | use App\Http\Controllers\Controller; |
| 7 | use App\Models\RefreshToken; |
| 8 | use Illuminate\Http\Request; |
| 9 | use Illuminate\Routing\Controllers\HasMiddleware; |
| 10 | use Illuminate\Routing\Controllers\Middleware; |
| 11 | use Illuminate\Support\Str; |
| 12 | |
| 13 | |
| 14 | class AuthController extends Controller implements HasMiddleware |
| 15 | { |
| 16 | |
| 17 | public static function middleware() |
| 18 | { |
| 19 | |
| 20 | return [ |
| 21 | new Middleware('auth:jwt', except: ['login', 'refreshToken']), |
| 22 | ]; |
| 23 | } |
| 24 | |
| 25 | /** |
| 26 | * Get a JWT via given credentials. |
| 27 | * |
| 28 | * @return \Illuminate\Http\JsonResponse |
| 29 | */ |
| 30 | public function login() |
| 31 | { |
| 32 | $credentials = request(['email', 'password']); |
| 33 | |
| 34 | if (!$token = auth('jwt')->attempt($credentials)) { |
| 35 | return response()->json(['error' => 'Unauthorized'], 401); |
| 36 | } |
| 37 | |
| 38 | $user = auth('jwt')->user(); |
| 39 | |
| 40 | $refreshToken = $this->createRefreshToken($user); |
| 41 | |
| 42 | return $this->respondWithTokens($token, $refreshToken); |
| 43 | } |
| 44 | |
| 45 | /** |
| 46 | * Get the authenticated User. |
| 47 | * |
| 48 | * @return \Illuminate\Http\JsonResponse |
| 49 | */ |
| 50 | public function me() |
| 51 | { |
| 52 | return response()->json(auth('jwt')->user()); |
| 53 | } |
| 54 | |
| 55 | /** |
| 56 | * Log the user out (Invalidate the token). |
| 57 | * |
| 58 | * @return \Illuminate\Http\JsonResponse |
| 59 | */ |
| 60 | public function logout() |
| 61 | { |
| 62 | auth('jwt')->logout(); |
| 63 | |
| 64 | return response()->json(['message' => 'Successfully logged out']); |
| 65 | } |
| 66 | |
| 67 | /** |
| 68 | * Refresh access token using refresh token. |
| 69 | * |
| 70 | * @return \Illuminate\Http\JsonResponse |
| 71 | */ |
| 72 | public function refreshToken(Request $request) |
| 73 | { |
| 74 | |
| 75 | $request->validate([ |
| 76 | 'refresh_token' => 'required|string', |
| 77 | ]); |
| 78 | |
| 79 | //$credentials = request(['email', 'password']); |
| 80 | |
| 81 | $refreshTokenModel = RefreshToken::where('token', $request->refresh_token) |
| 82 | ->where('expires_at', '>', now()) |
| 83 | ->first(); |
| 84 | |
| 85 | if (!$refreshTokenModel) { |
| 86 | return response()->json(['error' => 'Invalid or expired refresh token'], 401); |
| 87 | } |
| 88 | |
| 89 | $user = $refreshTokenModel->user; |
| 90 | |
| 91 | // Генерируем новый access token |
| 92 | $newAccessToken = auth('jwt')->login($user); |
| 93 | |
| 94 | // Создаем новый refresh token (опционально - можно переиспользовать старый) |
| 95 | $refreshTokenModel->delete(); // Удаляем старый |
| 96 | $newRefreshToken = $this->createRefreshToken($user); |
| 97 | |
| 98 | return $this->respondWithTokens($newAccessToken, $newRefreshToken); |
| 99 | } |
| 100 | |
| 101 | |
| 102 | /** |
| 103 | * Refresh a token. |
| 104 | * |
| 105 | * @return \Illuminate\Http\JsonResponse |
| 106 | */ |
| 107 | public function refresh() |
| 108 | { |
| 109 | $newToken = auth('jwt')->refresh(); |
| 110 | return $this->respondWithToken($newToken); |
| 111 | } |
| 112 | |
| 113 | /** |
| 114 | * Create refresh token for user. |
| 115 | * |
| 116 | * @param \App\Models\User $user |
| 117 | * @return string |
| 118 | */ |
| 119 | protected function createRefreshToken($user) |
| 120 | { |
| 121 | // Удаляем старые refresh токены пользователя |
| 122 | RefreshToken::where('user_id', $user->id)->delete(); |
| 123 | |
| 124 | $refreshToken = RefreshToken::create([ |
| 125 | 'user_id' => $user->id, |
| 126 | 'token' => Str::random(64), |
| 127 | 'expires_at' => now()->addDays(7), // 7 дней |
| 128 | ]); |
| 129 | |
| 130 | return $refreshToken->token; |
| 131 | } |
| 132 | |
| 133 | /** |
| 134 | * Get the token array structure with refresh token. |
| 135 | * |
| 136 | * @param string $accessToken |
| 137 | * @param string $refreshToken |
| 138 | * @return \Illuminate\Http\JsonResponse |
| 139 | */ |
| 140 | protected function respondWithTokens($accessToken, $refreshToken) |
| 141 | { |
| 142 | return response()->json([ |
| 143 | 'access_token' => $accessToken, |
| 144 | 'refresh_token' => $refreshToken, |
| 145 | 'token_type' => 'bearer', |
| 146 | 'expires_in' => auth('jwt')->factory()->getTTL() * 60, |
| 147 | 'refresh_expires_in' => 7 * 24 * 60 * 60, // 7 дней в секундах |
| 148 | ]); |
| 149 | } |
| 150 | |
| 151 | /** |
| 152 | * Get the token array structure (старый метод). |
| 153 | * |
| 154 | * @param string $token |
| 155 | * @return \Illuminate\Http\JsonResponse |
| 156 | */ |
| 157 | protected function respondWithToken($token) |
| 158 | { |
| 159 | return response()->json([ |
| 160 | 'access_token' => $token, |
| 161 | 'token_type' => 'bearer', |
| 162 | 'expires_in' => auth('jwt')->factory()->getTTL() * 60 |
| 163 | ]); |
| 164 | } |
| 165 | } |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 91.78% |
+ 67 / 73 |
+
+
+
+ |
+ 60.00% |
+ 3 / 5 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| CategoryController | +
+
+
+ |
+ 91.78% |
+ 67 / 73 |
+
+
+
+ |
+ 60.00% |
+ 3 / 5 |
+ 14.11 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| index | +
+
+
+ |
+ 88.46% |
+ 23 / 26 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 4.02 | ++ | ||
| store | +
+
+
+ |
+ 100.00% |
+ 18 / 18 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 3 | ++ | ||
| show | +
+
+
+ |
+ 40.00% |
+ 2 / 5 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2.86 | ++ | ||
| update | +
+
+
+ |
+ 100.00% |
+ 22 / 22 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 4 | ++ | ||
| destroy | +
+
+
+ |
+ 100.00% |
+ 2 / 2 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 1 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Http\API\V1; |
| 4 | |
| 5 | use App\Http\Controllers\Controller; |
| 6 | use App\Http\Resources\CategoryResource; |
| 7 | use App\Http\Resources\CategoryCollection; |
| 8 | use App\Models\Category; |
| 9 | use Illuminate\Http\Request; |
| 10 | use Illuminate\Support\Facades\Validator; |
| 11 | use Illuminate\Support\Str; |
| 12 | use Illuminate\Validation\Rule; |
| 13 | |
| 14 | /** |
| 15 | * @OA\Tag( |
| 16 | * name="Categories V1", |
| 17 | * description="API Endpoints for Category Management (Version 1)" |
| 18 | * ) |
| 19 | */ |
| 20 | class CategoryController extends Controller |
| 21 | { |
| 22 | /** |
| 23 | * @OA\Get( |
| 24 | * path="/api/v1/categories", |
| 25 | * operationId="getCategoriesListV1", |
| 26 | * tags={"Categories V1"}, |
| 27 | * summary="Get all categories", |
| 28 | * description="Returns paginated list of categories with optional products", |
| 29 | * @OA\Parameter( |
| 30 | * name="page", |
| 31 | * in="query", |
| 32 | * description="Page number", |
| 33 | * required=false, |
| 34 | * @OA\Schema(type="integer", default=1) |
| 35 | * ), |
| 36 | * @OA\Parameter( |
| 37 | * name="per_page", |
| 38 | * in="query", |
| 39 | * description="Items per page", |
| 40 | * required=false, |
| 41 | * @OA\Schema(type="integer", default=15) |
| 42 | * ), |
| 43 | * @OA\Parameter( |
| 44 | * name="with_products", |
| 45 | * in="query", |
| 46 | * description="Include products in response", |
| 47 | * required=false, |
| 48 | * @OA\Schema(type="boolean", default=false) |
| 49 | * ), |
| 50 | * @OA\Parameter( |
| 51 | * name="published", |
| 52 | * in="query", |
| 53 | * description="Filter by published status", |
| 54 | * required=false, |
| 55 | * @OA\Schema(type="boolean") |
| 56 | * ), |
| 57 | * @OA\Parameter( |
| 58 | * name="sort", |
| 59 | * in="query", |
| 60 | * description="Sort field (order, title, created_at)", |
| 61 | * required=false, |
| 62 | * @OA\Schema(type="string", default="order") |
| 63 | * ), |
| 64 | * @OA\Parameter( |
| 65 | * name="direction", |
| 66 | * in="query", |
| 67 | * description="Sort direction (asc, desc)", |
| 68 | * required=false, |
| 69 | * @OA\Schema(type="string", default="asc") |
| 70 | * ), |
| 71 | * @OA\Response( |
| 72 | * response=200, |
| 73 | * description="Successful operation", |
| 74 | * @OA\JsonContent(ref="#/components/schemas/CategoryCollection") |
| 75 | * ), |
| 76 | * @OA\Response( |
| 77 | * response=422, |
| 78 | * description="Validation error", |
| 79 | * @OA\JsonContent( |
| 80 | * @OA\Property(property="message", type="string"), |
| 81 | * @OA\Property(property="errors", type="object") |
| 82 | * ) |
| 83 | * ) |
| 84 | * ) |
| 85 | */ |
| 86 | public function index(Request $request) |
| 87 | { |
| 88 | $validator = Validator::make($request->all(), [ |
| 89 | 'per_page' => 'sometimes|integer|min:1|max:100', |
| 90 | 'with_products' => 'sometimes|boolean', |
| 91 | 'published' => 'sometimes|boolean', |
| 92 | 'sort' => 'sometimes|in:order,title,created_at', |
| 93 | 'direction' => 'sometimes|in:asc,desc', |
| 94 | ]); |
| 95 | |
| 96 | if ($validator->fails()) { |
| 97 | return response()->json([ |
| 98 | 'errors' => $validator->errors() |
| 99 | ], 422); |
| 100 | } |
| 101 | |
| 102 | $validated = $validator->validated(); |
| 103 | $perPage = $validated['per_page'] ?? 15; |
| 104 | $withProducts = $validated['with_products'] ?? false; |
| 105 | $sort = $validated['sort'] ?? 'order'; |
| 106 | $direction = $validated['direction'] ?? 'asc'; |
| 107 | |
| 108 | $query = Category::query(); |
| 109 | |
| 110 | if ($withProducts) { |
| 111 | $query->with(['products' => function($query) { |
| 112 | $query->select('products.id', 'title', 'price', 'image'); |
| 113 | }]); |
| 114 | } |
| 115 | |
| 116 | if (isset($validated['published'])) { |
| 117 | $query->where('published', $validated['published']); |
| 118 | } |
| 119 | |
| 120 | $categories = $query->orderBy($sort, $direction) |
| 121 | ->paginate($perPage); |
| 122 | |
| 123 | return new CategoryCollection($categories); |
| 124 | } |
| 125 | |
| 126 | /** |
| 127 | * @OA\Post( |
| 128 | * path="/api/v1/categories", |
| 129 | * operationId="createCategoryV1", |
| 130 | * tags={"Categories V1"}, |
| 131 | * summary="Create new category", |
| 132 | * description="Creates a new category", |
| 133 | * security={{"bearerAuth": {}}}, |
| 134 | * @OA\RequestBody( |
| 135 | * required=true, |
| 136 | * @OA\JsonContent(ref="#/components/schemas/CategoryRequest") |
| 137 | * ), |
| 138 | * @OA\Response( |
| 139 | * response=201, |
| 140 | * description="Category created successfully", |
| 141 | * @OA\JsonContent(ref="#/components/schemas/Category") |
| 142 | * ), |
| 143 | * @OA\Response( |
| 144 | * response=422, |
| 145 | * description="Validation error" |
| 146 | * ), |
| 147 | * @OA\Response( |
| 148 | * response=401, |
| 149 | * description="Unauthenticated" |
| 150 | * ) |
| 151 | * ) |
| 152 | */ |
| 153 | public function store(Request $request) |
| 154 | { |
| 155 | $validator = Validator::make($request->all(), [ |
| 156 | 'title' => 'required|string|max:255', |
| 157 | 'alias' => 'nullable|string|max:255|unique:categories,alias', |
| 158 | 'text' => 'nullable|string', |
| 159 | 'published' => 'boolean', |
| 160 | 'order' => 'nullable|integer', |
| 161 | ]); |
| 162 | |
| 163 | if ($validator->fails()) { |
| 164 | return response()->json([ |
| 165 | 'errors' => $validator->errors() |
| 166 | ], 422); |
| 167 | } |
| 168 | |
| 169 | $data = $validator->validated(); |
| 170 | |
| 171 | if (empty($data['alias'])) { |
| 172 | $data['alias'] = Str::slug($data['title']); |
| 173 | } |
| 174 | |
| 175 | $category = Category::create($data); |
| 176 | |
| 177 | return (new CategoryResource($category)) |
| 178 | ->response() |
| 179 | ->setStatusCode(201); |
| 180 | } |
| 181 | |
| 182 | /** |
| 183 | * @OA\Get( |
| 184 | * path="/api/v1/categories/{id}", |
| 185 | * operationId="getCategoryByIdV1", |
| 186 | * tags={"Categories V1"}, |
| 187 | * summary="Get category details", |
| 188 | * description="Returns category data with optional products", |
| 189 | * @OA\Parameter( |
| 190 | * name="id", |
| 191 | * in="path", |
| 192 | * description="Category ID", |
| 193 | * required=true, |
| 194 | * @OA\Schema(type="integer") |
| 195 | * ), |
| 196 | * @OA\Parameter( |
| 197 | * name="with_products", |
| 198 | * in="query", |
| 199 | * description="Include products in response", |
| 200 | * required=false, |
| 201 | * @OA\Schema(type="boolean", default=false) |
| 202 | * ), |
| 203 | * @OA\Response( |
| 204 | * response=200, |
| 205 | * description="Successful operation", |
| 206 | * @OA\JsonContent(ref="#/components/schemas/Category") |
| 207 | * ), |
| 208 | * @OA\Response( |
| 209 | * response=404, |
| 210 | * description="Category not found" |
| 211 | * ) |
| 212 | * ) |
| 213 | */ |
| 214 | public function show(Request $request, Category $category) |
| 215 | { |
| 216 | if ($request->boolean('with_products', false)) { |
| 217 | |
| 218 | $category->load(['products' => function($query) { |
| 219 | $query->select('products.id', 'title', 'price', 'image'); |
| 220 | }]); |
| 221 | } |
| 222 | |
| 223 | return new CategoryResource($category); |
| 224 | } |
| 225 | |
| 226 | /** |
| 227 | * @OA\Put( |
| 228 | * path="/api/v1/categories/{id}", |
| 229 | * operationId="updateCategoryV1", |
| 230 | * tags={"Categories V1"}, |
| 231 | * summary="Update existing category", |
| 232 | * description="Updates category record", |
| 233 | * security={{"bearerAuth": {}}}, |
| 234 | * @OA\Parameter( |
| 235 | * name="id", |
| 236 | * in="path", |
| 237 | * description="Category ID", |
| 238 | * required=true, |
| 239 | * @OA\Schema(type="integer") |
| 240 | * ), |
| 241 | * @OA\RequestBody( |
| 242 | * required=true, |
| 243 | * @OA\JsonContent(ref="#/components/schemas/CategoryRequest") |
| 244 | * ), |
| 245 | * @OA\Response( |
| 246 | * response=200, |
| 247 | * description="Category updated successfully", |
| 248 | * @OA\JsonContent(ref="#/components/schemas/Category") |
| 249 | * ), |
| 250 | * @OA\Response( |
| 251 | * response=404, |
| 252 | * description="Category not found" |
| 253 | * ), |
| 254 | * @OA\Response( |
| 255 | * response=422, |
| 256 | * description="Validation error" |
| 257 | * ), |
| 258 | * @OA\Response( |
| 259 | * response=401, |
| 260 | * description="Unauthenticated" |
| 261 | * ) |
| 262 | * ) |
| 263 | */ |
| 264 | public function update(Request $request, Category $category) |
| 265 | { |
| 266 | $validator = Validator::make($request->all(), [ |
| 267 | 'title' => 'sometimes|required|string|max:255', |
| 268 | 'alias' => [ |
| 269 | 'sometimes', |
| 270 | 'required', |
| 271 | 'string', |
| 272 | 'max:255', |
| 273 | Rule::unique('categories')->ignore($category->id) |
| 274 | ], |
| 275 | 'text' => 'nullable|string', |
| 276 | 'published' => 'boolean', |
| 277 | 'order' => 'nullable|integer', |
| 278 | ]); |
| 279 | |
| 280 | if ($validator->fails()) { |
| 281 | return response()->json([ |
| 282 | 'errors' => $validator->errors() |
| 283 | ], 422); |
| 284 | } |
| 285 | |
| 286 | $data = $validator->validated(); |
| 287 | |
| 288 | if (isset($data['title']) && empty($data['alias'])) { |
| 289 | $data['alias'] = Str::slug($data['title']); |
| 290 | } |
| 291 | |
| 292 | $category->update($data); |
| 293 | |
| 294 | return new CategoryResource($category); |
| 295 | } |
| 296 | |
| 297 | /** |
| 298 | * @OA\Delete( |
| 299 | * path="/api/v1/categories/{id}", |
| 300 | * operationId="deleteCategoryV1", |
| 301 | * tags={"Categories V1"}, |
| 302 | * summary="Delete category", |
| 303 | * description="Deletes category record", |
| 304 | * security={{"bearerAuth": {}}}, |
| 305 | * @OA\Parameter( |
| 306 | * name="id", |
| 307 | * in="path", |
| 308 | * description="Category ID", |
| 309 | * required=true, |
| 310 | * @OA\Schema(type="integer") |
| 311 | * ), |
| 312 | * @OA\Response( |
| 313 | * response=204, |
| 314 | * description="Category deleted successfully" |
| 315 | * ), |
| 316 | * @OA\Response( |
| 317 | * response=404, |
| 318 | * description="Category not found" |
| 319 | * ), |
| 320 | * @OA\Response( |
| 321 | * response=401, |
| 322 | * description="Unauthenticated" |
| 323 | * ) |
| 324 | * ) |
| 325 | */ |
| 326 | public function destroy(Category $category) |
| 327 | { |
| 328 | $category->delete(); |
| 329 | return response()->json(null, 204); |
| 330 | } |
| 331 | } |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 88.57% |
+ 31 / 35 |
+
+
+
+ |
+ 66.67% |
+ 2 / 3 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| PersonalCabinetController | +
+
+
+ |
+ 88.57% |
+ 31 / 35 |
+
+
+
+ |
+ 66.67% |
+ 2 / 3 |
+ 8.10 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| index | +
+
+
+ |
+ 100.00% |
+ 5 / 5 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 1 | ++ | ||
| update | +
+
+
+ |
+ 83.33% |
+ 20 / 24 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 5.12 | ++ | ||
| destroy | +
+
+
+ |
+ 100.00% |
+ 6 / 6 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 2 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Http\API\V1; |
| 4 | |
| 5 | use App\Http\Controllers\Controller; |
| 6 | use App\Models\User; |
| 7 | use App\Models\UserProfile; |
| 8 | use Illuminate\Http\Request; |
| 9 | use Illuminate\Support\Facades\Validator; |
| 10 | use Illuminate\Contracts\Auth\Authenticatable; |
| 11 | |
| 12 | class PersonalCabinetController extends Controller |
| 13 | { |
| 14 | /** |
| 15 | * Получить данные пользователя |
| 16 | * |
| 17 | * @param Authenticatable $user Авторизованный пользователь |
| 18 | * @return \Illuminate\Http\JsonResponse |
| 19 | */ |
| 20 | public function index(Authenticatable $user) |
| 21 | { |
| 22 | $user = $user->load('profile'); |
| 23 | |
| 24 | return response()->json([ |
| 25 | 'user' => $user, |
| 26 | 'profile' => $user->profile, |
| 27 | ]); |
| 28 | } |
| 29 | |
| 30 | /** |
| 31 | * Обновить профиль пользователя |
| 32 | * |
| 33 | * @param Request $request |
| 34 | * @param User $user Авторизованный пользователь |
| 35 | * @return \Illuminate\Http\JsonResponse |
| 36 | */ |
| 37 | public function update(Request $request, Authenticatable $user) |
| 38 | { |
| 39 | $validator = Validator::make($request->all(), [ |
| 40 | 'name' => 'sometimes|string|max:255', |
| 41 | 'email' => 'sometimes|string|email|max:255|unique:users,email,' . $user->id, |
| 42 | 'phone' => 'nullable|string|max:20', |
| 43 | 'address' => 'nullable|string|max:255', |
| 44 | 'birth_date' => 'nullable|date', |
| 45 | 'about' => 'nullable|string', |
| 46 | ]); |
| 47 | |
| 48 | if ($validator->fails()) { |
| 49 | return response()->json($validator->errors(), 422); |
| 50 | } |
| 51 | |
| 52 | // Обновляем основные данные пользователя |
| 53 | if ($request->has('name')) { |
| 54 | $user->name = $request->name; |
| 55 | } |
| 56 | |
| 57 | if ($request->has('email')) { |
| 58 | $user->email = $request->email; |
| 59 | } |
| 60 | |
| 61 | $user->save(); |
| 62 | |
| 63 | // Обновляем или создаем профиль |
| 64 | $profileData = $request->only(['phone', 'address', 'birth_date', 'about']); |
| 65 | |
| 66 | if ($user->profile) { |
| 67 | $user->profile->update($profileData); |
| 68 | } else { |
| 69 | $profileData['user_id'] = $user->id; |
| 70 | UserProfile::create($profileData); |
| 71 | } |
| 72 | |
| 73 | return response()->json([ |
| 74 | 'message' => 'Profile updated successfully', |
| 75 | 'user' => $user->load('profile'), |
| 76 | ]); |
| 77 | } |
| 78 | |
| 79 | /** |
| 80 | * Удалить аккаунт пользователя |
| 81 | * |
| 82 | * @param User $user Авторизованный пользователь |
| 83 | * @return \Illuminate\Http\JsonResponse |
| 84 | */ |
| 85 | public function destroy() |
| 86 | { |
| 87 | // Получаем аутентифицированного пользователя |
| 88 | $user = auth('jwt')->user(); |
| 89 | |
| 90 | if ($user) { |
| 91 | $user->delete(); |
| 92 | } |
| 93 | |
| 94 | return response()->json([ |
| 95 | 'message' => 'Account deleted successfully', |
| 96 | ]); |
| 97 | } |
| 98 | } |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 97.94% |
+ 95 / 97 |
+
+
+
+ |
+ 60.00% |
+ 3 / 5 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| ProductsController | +
+
+
+ |
+ 97.94% |
+ 95 / 97 |
+
+
+
+ |
+ 60.00% |
+ 3 / 5 |
+ 18 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| index | +
+
+
+ |
+ 96.67% |
+ 29 / 30 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 5 | ++ | ||
| store | +
+
+
+ |
+ 100.00% |
+ 29 / 29 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 5 | ++ | ||
| show | +
+
+
+ |
+ 100.00% |
+ 4 / 4 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 1 | ++ | ||
| update | +
+
+
+ |
+ 96.88% |
+ 31 / 32 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 6 | ++ | ||
| destroy | +
+
+
+ |
+ 100.00% |
+ 2 / 2 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 1 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Http\API\V1; |
| 4 | |
| 5 | use App\Http\Controllers\Controller; |
| 6 | use App\Http\Resources\ProductCollection; |
| 7 | use App\Http\Resources\ProductResource; |
| 8 | use App\Models\Product; |
| 9 | use Illuminate\Http\Request; |
| 10 | use Illuminate\Support\Facades\Validator; |
| 11 | use Illuminate\Support\Str; |
| 12 | use Illuminate\Validation\Rule; |
| 13 | |
| 14 | /** |
| 15 | * @OA\Tag( |
| 16 | * name="Products V1", |
| 17 | * description="API Endpoints for Product Management (Version 1)" |
| 18 | * ) |
| 19 | */ |
| 20 | class ProductsController extends Controller |
| 21 | { |
| 22 | /** |
| 23 | * @OA\Get( |
| 24 | * path="/api/v1/products", |
| 25 | * operationId="getProductsListV1", |
| 26 | * tags={"Products V1"}, |
| 27 | * summary="Get all products", |
| 28 | * description="Returns paginated list of products with their categories", |
| 29 | * @OA\Parameter( |
| 30 | * name="page", |
| 31 | * in="query", |
| 32 | * description="Page number", |
| 33 | * required=false, |
| 34 | * @OA\Schema(type="integer", default=1) |
| 35 | * ), |
| 36 | * @OA\Parameter( |
| 37 | * name="per_page", |
| 38 | * in="query", |
| 39 | * description="Items per page (default: 15)", |
| 40 | * required=false, |
| 41 | * @OA\Schema(type="integer", default=15) |
| 42 | * ), |
| 43 | * @OA\Parameter( |
| 44 | * name="category_id", |
| 45 | * in="query", |
| 46 | * description="Filter by category ID", |
| 47 | * required=false, |
| 48 | * @OA\Schema(type="integer") |
| 49 | * ), |
| 50 | * @OA\Parameter( |
| 51 | * name="is_sale", |
| 52 | * in="query", |
| 53 | * description="Filter by sale status", |
| 54 | * required=false, |
| 55 | * @OA\Schema(type="boolean") |
| 56 | * ), |
| 57 | * @OA\Parameter( |
| 58 | * name="published", |
| 59 | * in="query", |
| 60 | * description="Filter by published status", |
| 61 | * required=false, |
| 62 | * @OA\Schema(type="boolean") |
| 63 | * ), |
| 64 | * @OA\Parameter( |
| 65 | * name="sort", |
| 66 | * in="query", |
| 67 | * description="Sort field (price, order, created_at)", |
| 68 | * required=false, |
| 69 | * @OA\Schema(type="string", default="order") |
| 70 | * ), |
| 71 | * @OA\Parameter( |
| 72 | * name="direction", |
| 73 | * in="query", |
| 74 | * description="Sort direction (asc, desc)", |
| 75 | * required=false, |
| 76 | * @OA\Schema(type="string", default="asc") |
| 77 | * ), |
| 78 | * @OA\Response( |
| 79 | * response=200, |
| 80 | * description="Successful operation", |
| 81 | * @OA\JsonContent(ref="#/components/schemas/ProductCollection") |
| 82 | * ) |
| 83 | * ) |
| 84 | */ |
| 85 | public function index(Request $request) |
| 86 | { |
| 87 | $validator = Validator::make($request->all(), [ |
| 88 | 'per_page' => 'sometimes|integer|min:1|max:100', |
| 89 | 'category_id' => 'sometimes|integer|exists:categories,id', |
| 90 | 'is_sale' => 'sometimes|boolean', |
| 91 | 'published' => 'sometimes|boolean', |
| 92 | 'sort' => 'sometimes|in:price,order,created_at,title', |
| 93 | 'direction' => 'sometimes|in:asc,desc', |
| 94 | ]); |
| 95 | |
| 96 | if ($validator->fails()) { |
| 97 | return response()->json([ |
| 98 | 'errors' => $validator->errors() |
| 99 | ], 422); |
| 100 | } |
| 101 | |
| 102 | $validated = $validator->validated(); |
| 103 | $perPage = $validated['per_page'] ?? 15; |
| 104 | $sort = $validated['sort'] ?? 'order'; |
| 105 | $direction = $validated['direction'] ?? 'asc'; |
| 106 | |
| 107 | $query = Product::with(['categories' => function($query) { |
| 108 | $query->select('id', 'title', 'alias'); |
| 109 | }]); |
| 110 | |
| 111 | if (isset($validated['category_id'])) { |
| 112 | $query->whereHas('categories', function($q) use ($validated) { |
| 113 | $q->where('id', $validated['category_id']); |
| 114 | }); |
| 115 | } |
| 116 | |
| 117 | if (isset($validated['is_sale'])) { |
| 118 | $query->where('is_sale', $validated['is_sale']); |
| 119 | } |
| 120 | |
| 121 | if (isset($validated['published'])) { |
| 122 | $query->where('published', $validated['published']); |
| 123 | } |
| 124 | |
| 125 | $products = $query->orderBy($sort, $direction) |
| 126 | ->paginate($perPage); |
| 127 | |
| 128 | return new ProductCollection($products); |
| 129 | } |
| 130 | |
| 131 | /** |
| 132 | * @OA\Post( |
| 133 | * path="/api/v1/products", |
| 134 | * operationId="createProductV1", |
| 135 | * tags={"Products V1"}, |
| 136 | * summary="Create new product", |
| 137 | * description="Creates a new product with categories", |
| 138 | * security={{"bearerAuth": {}}}, |
| 139 | * @OA\RequestBody( |
| 140 | * required=true, |
| 141 | * @OA\JsonContent(ref="#/components/schemas/ProductRequest") |
| 142 | * ), |
| 143 | * @OA\Response( |
| 144 | * response=201, |
| 145 | * description="Product created successfully", |
| 146 | * @OA\JsonContent(ref="#/components/schemas/Product") |
| 147 | * ), |
| 148 | * @OA\Response( |
| 149 | * response=422, |
| 150 | * description="Validation error", |
| 151 | * @OA\JsonContent( |
| 152 | * @OA\Property(property="message", type="string", example="The given data was invalid."), |
| 153 | * @OA\Property( |
| 154 | * property="errors", |
| 155 | * type="object", |
| 156 | * @OA\Property( |
| 157 | * property="title", |
| 158 | * type="array", |
| 159 | * @OA\Items(type="string", example="The title field is required.") |
| 160 | * ) |
| 161 | * ) |
| 162 | * ) |
| 163 | * ), |
| 164 | * @OA\Response( |
| 165 | * response=401, |
| 166 | * description="Unauthenticated" |
| 167 | * ) |
| 168 | * ) |
| 169 | */ |
| 170 | public function store(Request $request) |
| 171 | { |
| 172 | $validator = Validator::make($request->all(), [ |
| 173 | 'title' => 'required|string|max:255', |
| 174 | 'alias' => 'nullable|string|max:255|unique:products,alias', |
| 175 | 'text' => 'nullable|string', |
| 176 | 'image' => 'nullable|string|max:255', |
| 177 | 'images' => 'nullable|array', |
| 178 | 'is_sale' => 'boolean', |
| 179 | 'published' => 'boolean', |
| 180 | 'order' => 'nullable|integer', |
| 181 | 'price' => 'required|numeric|min:0', |
| 182 | 'categories' => 'nullable|array', |
| 183 | 'categories.*' => 'exists:categories,id', |
| 184 | ]); |
| 185 | |
| 186 | if ($validator->fails()) { |
| 187 | return response()->json([ |
| 188 | 'errors' => $validator->errors() |
| 189 | ], 422); |
| 190 | } |
| 191 | |
| 192 | $data = $validator->validated(); |
| 193 | |
| 194 | if (empty($data['alias'])) { |
| 195 | $data['alias'] = Str::slug($data['title']); |
| 196 | } |
| 197 | |
| 198 | if (isset($data['images'])) { |
| 199 | $data['images'] = json_encode($data['images']); |
| 200 | } |
| 201 | |
| 202 | $product = Product::create($data); |
| 203 | |
| 204 | if (isset($data['categories'])) { |
| 205 | $product->categories()->sync($data['categories']); |
| 206 | } |
| 207 | |
| 208 | // Загружаем категории для нового продукта |
| 209 | $product->load('categories'); |
| 210 | |
| 211 | return (new ProductResource($product)) |
| 212 | ->response() |
| 213 | ->setStatusCode(201); |
| 214 | } |
| 215 | |
| 216 | /** |
| 217 | * @OA\Get( |
| 218 | * path="/api/v1/products/{id}", |
| 219 | * operationId="getProductByIdV1", |
| 220 | * tags={"Products V1"}, |
| 221 | * summary="Get product details", |
| 222 | * description="Returns product data with categories", |
| 223 | * @OA\Parameter( |
| 224 | * name="id", |
| 225 | * in="path", |
| 226 | * description="Product ID", |
| 227 | * required=true, |
| 228 | * @OA\Schema(type="integer") |
| 229 | * ), |
| 230 | * @OA\Response( |
| 231 | * response=200, |
| 232 | * description="Successful operation", |
| 233 | * @OA\JsonContent(ref="#/components/schemas/Product") |
| 234 | * ), |
| 235 | * @OA\Response( |
| 236 | * response=404, |
| 237 | * description="Product not found" |
| 238 | * ) |
| 239 | * ) |
| 240 | */ |
| 241 | public function show(Product $product) |
| 242 | { |
| 243 | $product->load(['categories' => function($query) { |
| 244 | $query->select('id', 'title', 'alias'); |
| 245 | }]); |
| 246 | return new ProductResource($product); |
| 247 | } |
| 248 | |
| 249 | /** |
| 250 | * @OA\Put( |
| 251 | * path="/api/v1/products/{id}", |
| 252 | * operationId="updateProductV1", |
| 253 | * tags={"Products V1"}, |
| 254 | * summary="Update existing product", |
| 255 | * description="Updates product and its categories", |
| 256 | * security={{"bearerAuth": {}}}, |
| 257 | * @OA\Parameter( |
| 258 | * name="id", |
| 259 | * in="path", |
| 260 | * description="Product ID", |
| 261 | * required=true, |
| 262 | * @OA\Schema(type="integer") |
| 263 | * ), |
| 264 | * @OA\RequestBody( |
| 265 | * required=true, |
| 266 | * @OA\JsonContent(ref="#/components/schemas/ProductRequest") |
| 267 | * ), |
| 268 | * @OA\Response( |
| 269 | * response=200, |
| 270 | * description="Product updated successfully", |
| 271 | * @OA\JsonContent(ref="#/components/schemas/Product") |
| 272 | * ), |
| 273 | * @OA\Response( |
| 274 | * response=404, |
| 275 | * description="Product not found" |
| 276 | * ), |
| 277 | * @OA\Response( |
| 278 | * response=422, |
| 279 | * description="Validation error" |
| 280 | * ), |
| 281 | * @OA\Response( |
| 282 | * response=401, |
| 283 | * description="Unauthenticated" |
| 284 | * ) |
| 285 | * ) |
| 286 | */ |
| 287 | public function update(Request $request, Product $product) |
| 288 | { |
| 289 | $validator = Validator::make($request->all(), [ |
| 290 | 'title' => 'sometimes|required|string|max:255', |
| 291 | 'alias' => [ |
| 292 | 'sometimes', |
| 293 | 'required', |
| 294 | 'string', |
| 295 | 'max:255', |
| 296 | Rule::unique('products')->ignore($product->id) |
| 297 | ], |
| 298 | 'text' => 'nullable|string', |
| 299 | 'image' => 'nullable|string|max:255', |
| 300 | 'images' => 'nullable|array', |
| 301 | 'is_sale' => 'boolean', |
| 302 | 'published' => 'boolean', |
| 303 | 'order' => 'nullable|integer', |
| 304 | 'price' => 'sometimes|required|numeric|min:0', |
| 305 | 'categories' => 'nullable|array', |
| 306 | 'categories.*' => 'exists:categories,id', |
| 307 | ]); |
| 308 | |
| 309 | if ($validator->fails()) { |
| 310 | return response()->json([ |
| 311 | 'errors' => $validator->errors() |
| 312 | ], 422); |
| 313 | } |
| 314 | |
| 315 | $data = $validator->validated(); |
| 316 | |
| 317 | if (isset($data['images'])) { |
| 318 | $data['images'] = json_encode($data['images']); |
| 319 | } |
| 320 | |
| 321 | if (isset($data['price']) && $data['price'] != $product->price) { |
| 322 | // You can add price change tracking logic here |
| 323 | } |
| 324 | |
| 325 | $product->update($data); |
| 326 | |
| 327 | if (isset($data['categories'])) { |
| 328 | $product->categories()->sync($data['categories']); |
| 329 | } |
| 330 | |
| 331 | $product->load('categories'); |
| 332 | |
| 333 | return new ProductResource($product->fresh()->load('categories')); |
| 334 | } |
| 335 | |
| 336 | /** |
| 337 | * @OA\Delete( |
| 338 | * path="/api/v1/products/{id}", |
| 339 | * operationId="deleteProductV1", |
| 340 | * tags={"Products V1"}, |
| 341 | * summary="Delete product", |
| 342 | * description="Deletes a product", |
| 343 | * security={{"bearerAuth": {}}}, |
| 344 | * @OA\Parameter( |
| 345 | * name="id", |
| 346 | * in="path", |
| 347 | * description="Product ID", |
| 348 | * required=true, |
| 349 | * @OA\Schema(type="integer") |
| 350 | * ), |
| 351 | * @OA\Response( |
| 352 | * response=204, |
| 353 | * description="Product deleted successfully" |
| 354 | * ), |
| 355 | * @OA\Response( |
| 356 | * response=404, |
| 357 | * description="Product not found" |
| 358 | * ), |
| 359 | * @OA\Response( |
| 360 | * response=401, |
| 361 | * description="Unauthenticated" |
| 362 | * ) |
| 363 | * ) |
| 364 | */ |
| 365 | public function destroy(Product $product) |
| 366 | { |
| 367 | $product->delete(); |
| 368 | return response()->json(null, 204); |
| 369 | } |
| 370 | } |
| Class | +Coverage | +
|---|---|
| App\Http\API\V1\PersonalCabinetController | 88% |
| Class | +CRAP | +
|---|---|
| App\Http\API\V1\PersonalCabinetController | 8 |
| + | Code Coverage |
+ ||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ ||||||
| Total | +
+
+
+ |
+ 95.22% |
+ 239 / 251 |
+
+
+
+ |
+ 77.27% |
+ 17 / 22 |
+
+
+
+ |
+ 25.00% |
+ 1 / 4 |
+
|
+
+
+ |
+ 100.00% |
+ 46 / 46 |
+
+
+
+ |
+ 100.00% |
+ 9 / 9 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ |
|
+
+
+ |
+ 91.78% |
+ 67 / 73 |
+
+
+
+ |
+ 60.00% |
+ 3 / 5 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
|
+
+
+ |
+ 88.57% |
+ 31 / 35 |
+
+
+
+ |
+ 66.67% |
+ 2 / 3 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
|
+
+
+ |
+ 97.94% |
+ 95 / 97 |
+
+
+
+ |
+ 60.00% |
+ 3 / 5 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
| Class | +Coverage | +
|---|---|
| App\Http\API\V1\PersonalCabinetController | 88% |
| Class | +CRAP | +
|---|---|
| App\Http\API\V1\PersonalCabinetController | 8 |
| + | Code Coverage |
+ ||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ ||||||
| Total | +
+
+
+ |
+ 95.22% |
+ 239 / 251 |
+
+
+
+ |
+ 77.27% |
+ 17 / 22 |
+
+
+
+ |
+ 25.00% |
+ 1 / 4 |
+
|
+
+
+ |
+ 95.22% |
+ 239 / 251 |
+
+
+
+ |
+ 77.27% |
+ 17 / 22 |
+
+
+
+ |
+ 25.00% |
+ 1 / 4 |
+ |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 32 |
+
+
+
+ |
+ 0.00% |
+ 0 / 7 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| CategoryController | +
+
+
+ |
+ 0.00% |
+ 0 / 32 |
+
+
+
+ |
+ 0.00% |
+ 0 / 7 |
+ 56 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| __construct | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| index | +
+
+
+ |
+ 0.00% |
+ 0 / 3 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| create | +
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| store | +
+
+
+ |
+ 0.00% |
+ 0 / 11 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| edit | +
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| update | +
+
+
+ |
+ 0.00% |
+ 0 / 10 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| destroy | +
+
+
+ |
+ 0.00% |
+ 0 / 3 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Http\Controllers\Admin; |
| 4 | |
| 5 | use App\Http\Controllers\Controller; |
| 6 | use App\Models\Category; |
| 7 | use Illuminate\Http\Request; |
| 8 | use App\Models\User; |
| 9 | use App\Repositories\Contracts\CategoryRepositoryInterface; |
| 10 | use Illuminate\Support\Facades\Gate; |
| 11 | |
| 12 | class CategoryController extends Controller |
| 13 | { |
| 14 | protected CategoryRepositoryInterface $categoryRepository; |
| 15 | |
| 16 | |
| 17 | public function __construct( |
| 18 | CategoryRepositoryInterface $categoryRepository, |
| 19 | ) |
| 20 | { |
| 21 | $this->categoryRepository = $categoryRepository; |
| 22 | |
| 23 | } |
| 24 | |
| 25 | /** |
| 26 | * Display a listing of the resource. |
| 27 | */ |
| 28 | public function index() |
| 29 | { |
| 30 | Gate::authorize('viewAny', Category::class); |
| 31 | $categories = $this->categoryRepository->getAllPaginated(10); |
| 32 | return view('admin.categories.index', compact('categories')); |
| 33 | } |
| 34 | |
| 35 | /** |
| 36 | * Show the form for creating a new resource. |
| 37 | */ |
| 38 | public function create() |
| 39 | { |
| 40 | Gate::authorize('create', Category::class); |
| 41 | return view('admin.categories.create'); |
| 42 | } |
| 43 | |
| 44 | /** |
| 45 | * Store a newly created resource in storage. |
| 46 | */ |
| 47 | public function store(Request $request, User $user) |
| 48 | { |
| 49 | Gate::authorize('create', Category::class); |
| 50 | |
| 51 | $validated = $request->validate([ |
| 52 | 'title' => 'required|string|max:255', |
| 53 | 'alias' => 'required|string|max:255|unique:categories,alias', |
| 54 | 'text' => 'nullable|string', |
| 55 | 'published' => 'boolean', |
| 56 | 'order' => 'required|integer|min:0', |
| 57 | ]); |
| 58 | |
| 59 | $validated['user_id'] = $user->id; |
| 60 | |
| 61 | $this->categoryRepository->create($validated); |
| 62 | |
| 63 | return redirect()->route('admin.categories.index')->with('success', 'Category created successfully!'); |
| 64 | } |
| 65 | |
| 66 | /** |
| 67 | * Show the form for editing the specified resource. |
| 68 | */ |
| 69 | public function edit(Category $category) // Category model still resolved by route model binding |
| 70 | { |
| 71 | Gate::authorize('update', $category); |
| 72 | return view('admin.categories.edit', compact('category')); |
| 73 | } |
| 74 | |
| 75 | /** |
| 76 | * Update the specified resource in storage. |
| 77 | */ |
| 78 | public function update(Request $request, Category $category) // Category model still resolved by route model binding |
| 79 | { |
| 80 | Gate::authorize('update', $category); |
| 81 | $validated = $request->validate([ |
| 82 | 'title' => 'required|string|max:255', |
| 83 | 'alias' => 'required|string|max:255|unique:categories,alias,' . $category->id, |
| 84 | 'text' => 'nullable|string', |
| 85 | 'published' => 'boolean', |
| 86 | 'order' => 'required|integer|min:0', |
| 87 | ]); |
| 88 | |
| 89 | $this->categoryRepository->update($category, $validated); |
| 90 | |
| 91 | return redirect()->route('admin.categories.index')->with('success', 'Category updated successfully!'); |
| 92 | } |
| 93 | |
| 94 | /** |
| 95 | * Remove the specified resource from storage. |
| 96 | */ |
| 97 | public function destroy(Category $category) // Category model still resolved by route model binding |
| 98 | { |
| 99 | Gate::authorize('delete', $category); |
| 100 | |
| 101 | $this->categoryRepository->delete($category); |
| 102 | |
| 103 | return redirect()->route('admin.categories.index')->with('success', 'Category deleted successfully!'); |
| 104 | } |
| 105 | } |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 82 |
+
+
+
+ |
+ 0.00% |
+ 0 / 7 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| ProductController | +
+
+
+ |
+ 0.00% |
+ 0 / 82 |
+
+
+
+ |
+ 0.00% |
+ 0 / 7 |
+ 462 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| __construct | +
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| index | +
+
+
+ |
+ 0.00% |
+ 0 / 8 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| create | +
+
+
+ |
+ 0.00% |
+ 0 / 3 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| store | +
+
+
+ |
+ 0.00% |
+ 0 / 27 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 20 | ++ | ||
| edit | +
+
+
+ |
+ 0.00% |
+ 0 / 3 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| update | +
+
+
+ |
+ 0.00% |
+ 0 / 29 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 56 | ++ | ||
| destroy | +
+
+
+ |
+ 0.00% |
+ 0 / 10 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 42 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Http\Controllers\Admin; |
| 4 | |
| 5 | use App\Http\Controllers\Controller; |
| 6 | use App\Models\Product; |
| 7 | use App\Models\User; |
| 8 | use Illuminate\Http\Request; |
| 9 | use Illuminate\Support\Facades\Cache; |
| 10 | use Illuminate\Support\Facades\Gate; |
| 11 | use Illuminate\Support\Facades\Storage; |
| 12 | use App\Repositories\Contracts\ProductRepositoryInterface; |
| 13 | use App\Repositories\Contracts\CategoryRepositoryInterface; |
| 14 | |
| 15 | class ProductController extends Controller |
| 16 | { |
| 17 | protected ProductRepositoryInterface $productRepository; |
| 18 | protected CategoryRepositoryInterface $categoryRepository; |
| 19 | |
| 20 | |
| 21 | public function __construct( |
| 22 | ProductRepositoryInterface $productRepository, |
| 23 | CategoryRepositoryInterface $categoryRepository, |
| 24 | Gate $gate |
| 25 | ) { |
| 26 | $this->productRepository = $productRepository; |
| 27 | $this->categoryRepository = $categoryRepository; |
| 28 | } |
| 29 | |
| 30 | /** |
| 31 | * Display a listing of the resource. |
| 32 | */ |
| 33 | public function index(Request $request) |
| 34 | { |
| 35 | Gate::authorize('viewAny', Product::class); |
| 36 | $page = $request->get('page', 1); |
| 37 | $perPage = 10; |
| 38 | |
| 39 | $cacheKey = 'products_admin_page_' . $page . '_per_page_' . $perPage; |
| 40 | |
| 41 | $products = Cache::tags(['products'])->remember($cacheKey, 60, function () use ($perPage) { |
| 42 | return $this->productRepository->getAllPaginated($perPage); |
| 43 | }); |
| 44 | |
| 45 | return view('admin.products.index', compact('products')); |
| 46 | } |
| 47 | |
| 48 | /** |
| 49 | * Show the form for creating a new resource. |
| 50 | */ |
| 51 | public function create() |
| 52 | { |
| 53 | Gate::authorize('create', Product::class); |
| 54 | $categories = $this->categoryRepository->getAll(); // Get all categories via repository |
| 55 | return view('admin.products.create', compact('categories')); |
| 56 | } |
| 57 | |
| 58 | /** |
| 59 | * Store a newly created resource in storage. |
| 60 | */ |
| 61 | public function store(Request $request, User $user) |
| 62 | { |
| 63 | Gate::authorize('create', Product::class); |
| 64 | $validated = $request->validate([ |
| 65 | 'title' => 'required|string|max:255', |
| 66 | 'alias' => 'required|string|max:255|unique:products,alias', |
| 67 | 'text' => 'nullable|string', |
| 68 | 'image_file' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048', |
| 69 | 'images_files.*' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048', |
| 70 | 'is_sale' => 'boolean', |
| 71 | 'published' => 'boolean', |
| 72 | 'order' => 'required|integer|min:0', |
| 73 | 'price' => 'required|numeric|min:0', |
| 74 | 'categories' => 'nullable|array', |
| 75 | 'categories.*' => 'exists:categories,id', |
| 76 | ]); |
| 77 | |
| 78 | $data = $request->except(['_token', 'image_file', 'images_files', 'categories']); |
| 79 | |
| 80 | if ($request->hasFile('image_file')) { |
| 81 | $data['image'] = Storage::disk('public')->put('products', $request->file('image_file')); |
| 82 | } |
| 83 | |
| 84 | $uploadedImages = []; |
| 85 | if ($request->hasFile('images_files')) { |
| 86 | foreach ($request->file('images_files') as $file) { |
| 87 | $uploadedImages[] = Storage::disk('public')->put('products', $file); |
| 88 | } |
| 89 | $data['images'] = json_encode($uploadedImages); |
| 90 | } |
| 91 | |
| 92 | $data['user_id'] = $user->id; |
| 93 | |
| 94 | $product = $this->productRepository->create($data); |
| 95 | $this->productRepository->syncCategories($product, $request->input('categories', [])); |
| 96 | |
| 97 | Cache::tags(['products'])->flush(); |
| 98 | |
| 99 | return redirect()->route('admin.products.index')->with('success', 'Product created successfully!'); |
| 100 | } |
| 101 | |
| 102 | /** |
| 103 | * Show the form for editing the specified resource. |
| 104 | */ |
| 105 | public function edit(Product $product) |
| 106 | { |
| 107 | Gate::authorize('update', $product); |
| 108 | $categories = $this->categoryRepository->getAll(); |
| 109 | return view('admin.products.edit', compact('product', 'categories')); |
| 110 | } |
| 111 | |
| 112 | /** |
| 113 | * Update the specified resource in storage. |
| 114 | */ |
| 115 | public function update(Request $request, Product $product) |
| 116 | { |
| 117 | Gate::authorize('update', $product); |
| 118 | $validated = $request->validate([ |
| 119 | 'title' => 'required|string|max:255', |
| 120 | 'alias' => 'required|string|max:255|unique:products,alias,' . $product->id, |
| 121 | 'text' => 'nullable|string', |
| 122 | 'image_file' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048', |
| 123 | 'images_files.*' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048', |
| 124 | 'is_sale' => 'boolean', |
| 125 | 'published' => 'boolean', |
| 126 | 'order' => 'required|integer|min:0', |
| 127 | 'price' => 'required|numeric|min:0', |
| 128 | 'categories' => 'nullable|array', |
| 129 | 'categories.*' => 'exists:categories,id', |
| 130 | ]); |
| 131 | |
| 132 | $data = $request->except(['_token', '_method', 'image_file', 'images_files', 'categories']); |
| 133 | |
| 134 | if ($request->hasFile('image_file')) { |
| 135 | if ($product->image && Storage::disk('public')->exists($product->image)) { |
| 136 | Storage::disk('public')->delete($product->image); |
| 137 | } |
| 138 | $data['image'] = Storage::disk('public')->put('products', $request->file('image_file')); |
| 139 | } |
| 140 | |
| 141 | $existingImages = $product->images ? $product->images : []; |
| 142 | $newUploadedImages = []; |
| 143 | if ($request->hasFile('images_files')) { |
| 144 | foreach ($request->file('images_files') as $file) { |
| 145 | $newUploadedImages[] = Storage::disk('public')->put('products', $file); |
| 146 | } |
| 147 | $data['images'] = json_encode(array_merge($existingImages, $newUploadedImages)); |
| 148 | } |
| 149 | |
| 150 | $this->productRepository->update($product, $data); |
| 151 | $this->productRepository->syncCategories($product, $request->input('categories', [])); |
| 152 | |
| 153 | Cache::tags(['products'])->flush(); |
| 154 | |
| 155 | return redirect()->route('admin.products.index')->with('success', 'Product updated successfully!'); |
| 156 | } |
| 157 | |
| 158 | /** |
| 159 | * Remove the specified resource from storage. |
| 160 | */ |
| 161 | public function destroy(Product $product) |
| 162 | { |
| 163 | Gate::authorize('delete', $product); |
| 164 | // Cleanup files before deleting the record |
| 165 | if ($product->image && Storage::disk('public')->exists($product->image)) { |
| 166 | Storage::disk('public')->delete($product->image); |
| 167 | } |
| 168 | if ($product->images) { |
| 169 | foreach ($product->images as $imagePath) { |
| 170 | if (Storage::disk('public')->exists($imagePath)) { |
| 171 | Storage::disk('public')->delete($imagePath); |
| 172 | } |
| 173 | } |
| 174 | } |
| 175 | |
| 176 | $this->productRepository->delete($product); |
| 177 | |
| 178 | Cache::tags(['products'])->flush(); |
| 179 | |
| 180 | return redirect()->route('admin.products.index')->with('success', 'Product deleted successfully!'); |
| 181 | } |
| 182 | } |
| Class | +Coverage | +
|---|---|
| App\Http\Controllers\Admin\CategoryController | 0% |
| App\Http\Controllers\Admin\ProductController | 0% |
| Class | +CRAP | +
|---|---|
| App\Http\Controllers\Admin\ProductController | 462 |
| + | Code Coverage |
+ ||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ ||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 114 |
+
+
+
+ |
+ 0.00% |
+ 0 / 14 |
+
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+
|
+
+
+ |
+ 0.00% |
+ 0 / 32 |
+
+
+
+ |
+ 0.00% |
+ 0 / 7 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
|
+
+
+ |
+ 0.00% |
+ 0 / 82 |
+
+
+
+ |
+ 0.00% |
+ 0 / 7 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 8 |
+
+
+
+ |
+ 0.00% |
+ 0 / 3 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| AuthenticatedSessionController | +
+
+
+ |
+ 0.00% |
+ 0 / 8 |
+
+
+
+ |
+ 0.00% |
+ 0 / 3 |
+ 12 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| create | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| store | +
+
+
+ |
+ 0.00% |
+ 0 / 3 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| destroy | +
+
+
+ |
+ 0.00% |
+ 0 / 4 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Http\Controllers\Auth; |
| 4 | |
| 5 | use App\Http\Controllers\Controller; |
| 6 | use App\Http\Requests\Auth\LoginRequest; |
| 7 | use Illuminate\Http\RedirectResponse; |
| 8 | use Illuminate\Http\Request; |
| 9 | use Illuminate\Support\Facades\Auth; |
| 10 | use Illuminate\View\View; |
| 11 | |
| 12 | class AuthenticatedSessionController extends Controller |
| 13 | { |
| 14 | /** |
| 15 | * Display the login view. |
| 16 | */ |
| 17 | public function create(): View |
| 18 | { |
| 19 | return view('auth.login'); |
| 20 | } |
| 21 | |
| 22 | /** |
| 23 | * Handle an incoming authentication request. |
| 24 | */ |
| 25 | public function store(LoginRequest $request): RedirectResponse |
| 26 | { |
| 27 | $request->authenticate(); |
| 28 | |
| 29 | $request->session()->regenerate(); |
| 30 | |
| 31 | return redirect()->intended(route('dashboard', absolute: false)); |
| 32 | } |
| 33 | |
| 34 | /** |
| 35 | * Destroy an authenticated session. |
| 36 | */ |
| 37 | public function destroy(Request $request): RedirectResponse |
| 38 | { |
| 39 | Auth::guard('web')->logout(); |
| 40 | |
| 41 | $request->session()->invalidate(); |
| 42 | |
| 43 | $request->session()->regenerateToken(); |
| 44 | |
| 45 | return redirect('/'); |
| 46 | } |
| 47 | } |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 10 |
+
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| ConfirmablePasswordController | +
+
+
+ |
+ 0.00% |
+ 0 / 10 |
+
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+ 12 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| show | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| store | +
+
+
+ |
+ 0.00% |
+ 0 / 9 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 6 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Http\Controllers\Auth; |
| 4 | |
| 5 | use App\Http\Controllers\Controller; |
| 6 | use Illuminate\Http\RedirectResponse; |
| 7 | use Illuminate\Http\Request; |
| 8 | use Illuminate\Support\Facades\Auth; |
| 9 | use Illuminate\Validation\ValidationException; |
| 10 | use Illuminate\View\View; |
| 11 | |
| 12 | class ConfirmablePasswordController extends Controller |
| 13 | { |
| 14 | /** |
| 15 | * Show the confirm password view. |
| 16 | */ |
| 17 | public function show(): View |
| 18 | { |
| 19 | return view('auth.confirm-password'); |
| 20 | } |
| 21 | |
| 22 | /** |
| 23 | * Confirm the user's password. |
| 24 | */ |
| 25 | public function store(Request $request): RedirectResponse |
| 26 | { |
| 27 | if (! Auth::guard('web')->validate([ |
| 28 | 'email' => $request->user()->email, |
| 29 | 'password' => $request->password, |
| 30 | ])) { |
| 31 | throw ValidationException::withMessages([ |
| 32 | 'password' => __('auth.password'), |
| 33 | ]); |
| 34 | } |
| 35 | |
| 36 | $request->session()->put('auth.password_confirmed_at', time()); |
| 37 | |
| 38 | return redirect()->intended(route('dashboard', absolute: false)); |
| 39 | } |
| 40 | } |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 4 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| EmailVerificationNotificationController | +
+
+
+ |
+ 0.00% |
+ 0 / 4 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 6 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| store | +
+
+
+ |
+ 0.00% |
+ 0 / 4 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 6 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Http\Controllers\Auth; |
| 4 | |
| 5 | use App\Http\Controllers\Controller; |
| 6 | use Illuminate\Http\RedirectResponse; |
| 7 | use Illuminate\Http\Request; |
| 8 | |
| 9 | class EmailVerificationNotificationController extends Controller |
| 10 | { |
| 11 | /** |
| 12 | * Send a new email verification notification. |
| 13 | */ |
| 14 | public function store(Request $request): RedirectResponse |
| 15 | { |
| 16 | if ($request->user()->hasVerifiedEmail()) { |
| 17 | return redirect()->intended(route('dashboard', absolute: false)); |
| 18 | } |
| 19 | |
| 20 | $request->user()->sendEmailVerificationNotification(); |
| 21 | |
| 22 | return back()->with('status', 'verification-link-sent'); |
| 23 | } |
| 24 | } |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 3 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| EmailVerificationPromptController | +
+
+
+ |
+ 0.00% |
+ 0 / 3 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 6 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| __invoke | +
+
+
+ |
+ 0.00% |
+ 0 / 3 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 6 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Http\Controllers\Auth; |
| 4 | |
| 5 | use App\Http\Controllers\Controller; |
| 6 | use Illuminate\Http\RedirectResponse; |
| 7 | use Illuminate\Http\Request; |
| 8 | use Illuminate\View\View; |
| 9 | |
| 10 | class EmailVerificationPromptController extends Controller |
| 11 | { |
| 12 | /** |
| 13 | * Display the email verification prompt. |
| 14 | */ |
| 15 | public function __invoke(Request $request): RedirectResponse|View |
| 16 | { |
| 17 | return $request->user()->hasVerifiedEmail() |
| 18 | ? redirect()->intended(route('dashboard', absolute: false)) |
| 19 | : view('auth.verify-email'); |
| 20 | } |
| 21 | } |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 20 |
+
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| NewPasswordController | +
+
+
+ |
+ 0.00% |
+ 0 / 20 |
+
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+ 12 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| create | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| store | +
+
+
+ |
+ 0.00% |
+ 0 / 19 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 6 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Http\Controllers\Auth; |
| 4 | |
| 5 | use App\Http\Controllers\Controller; |
| 6 | use App\Models\User; |
| 7 | use Illuminate\Auth\Events\PasswordReset; |
| 8 | use Illuminate\Http\RedirectResponse; |
| 9 | use Illuminate\Http\Request; |
| 10 | use Illuminate\Support\Facades\Hash; |
| 11 | use Illuminate\Support\Facades\Password; |
| 12 | use Illuminate\Support\Str; |
| 13 | use Illuminate\Validation\Rules; |
| 14 | use Illuminate\View\View; |
| 15 | |
| 16 | class NewPasswordController extends Controller |
| 17 | { |
| 18 | /** |
| 19 | * Display the password reset view. |
| 20 | */ |
| 21 | public function create(Request $request): View |
| 22 | { |
| 23 | return view('auth.reset-password', ['request' => $request]); |
| 24 | } |
| 25 | |
| 26 | /** |
| 27 | * Handle an incoming new password request. |
| 28 | * |
| 29 | * @throws \Illuminate\Validation\ValidationException |
| 30 | */ |
| 31 | public function store(Request $request): RedirectResponse |
| 32 | { |
| 33 | $request->validate([ |
| 34 | 'token' => ['required'], |
| 35 | 'email' => ['required', 'email'], |
| 36 | 'password' => ['required', 'confirmed', Rules\Password::defaults()], |
| 37 | ]); |
| 38 | |
| 39 | // Here we will attempt to reset the user's password. If it is successful we |
| 40 | // will update the password on an actual user model and persist it to the |
| 41 | // database. Otherwise we will parse the error and return the response. |
| 42 | $status = Password::reset( |
| 43 | $request->only('email', 'password', 'password_confirmation', 'token'), |
| 44 | function (User $user) use ($request) { |
| 45 | $user->forceFill([ |
| 46 | 'password' => Hash::make($request->password), |
| 47 | 'remember_token' => Str::random(60), |
| 48 | ])->save(); |
| 49 | |
| 50 | event(new PasswordReset($user)); |
| 51 | } |
| 52 | ); |
| 53 | |
| 54 | // If the password was successfully reset, we will redirect the user back to |
| 55 | // the application's home authenticated view. If there is an error we can |
| 56 | // redirect them back to where they came from with their error message. |
| 57 | return $status == Password::PASSWORD_RESET |
| 58 | ? redirect()->route('login')->with('status', __($status)) |
| 59 | : back()->withInput($request->only('email')) |
| 60 | ->withErrors(['email' => __($status)]); |
| 61 | } |
| 62 | } |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 8 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| PasswordController | +
+
+
+ |
+ 0.00% |
+ 0 / 8 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| update | +
+
+
+ |
+ 0.00% |
+ 0 / 8 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Http\Controllers\Auth; |
| 4 | |
| 5 | use App\Http\Controllers\Controller; |
| 6 | use Illuminate\Http\RedirectResponse; |
| 7 | use Illuminate\Http\Request; |
| 8 | use Illuminate\Support\Facades\Hash; |
| 9 | use Illuminate\Validation\Rules\Password; |
| 10 | |
| 11 | class PasswordController extends Controller |
| 12 | { |
| 13 | /** |
| 14 | * Update the user's password. |
| 15 | */ |
| 16 | public function update(Request $request): RedirectResponse |
| 17 | { |
| 18 | $validated = $request->validateWithBag('updatePassword', [ |
| 19 | 'current_password' => ['required', 'current_password'], |
| 20 | 'password' => ['required', Password::defaults(), 'confirmed'], |
| 21 | ]); |
| 22 | |
| 23 | $request->user()->update([ |
| 24 | 'password' => Hash::make($validated['password']), |
| 25 | ]); |
| 26 | |
| 27 | return back()->with('status', 'password-updated'); |
| 28 | } |
| 29 | } |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 11 |
+
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| PasswordResetLinkController | +
+
+
+ |
+ 0.00% |
+ 0 / 11 |
+
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+ 12 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| create | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| store | +
+
+
+ |
+ 0.00% |
+ 0 / 10 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 6 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Http\Controllers\Auth; |
| 4 | |
| 5 | use App\Http\Controllers\Controller; |
| 6 | use Illuminate\Http\RedirectResponse; |
| 7 | use Illuminate\Http\Request; |
| 8 | use Illuminate\Support\Facades\Password; |
| 9 | use Illuminate\View\View; |
| 10 | |
| 11 | class PasswordResetLinkController extends Controller |
| 12 | { |
| 13 | /** |
| 14 | * Display the password reset link request view. |
| 15 | */ |
| 16 | public function create(): View |
| 17 | { |
| 18 | return view('auth.forgot-password'); |
| 19 | } |
| 20 | |
| 21 | /** |
| 22 | * Handle an incoming password reset link request. |
| 23 | * |
| 24 | * @throws \Illuminate\Validation\ValidationException |
| 25 | */ |
| 26 | public function store(Request $request): RedirectResponse |
| 27 | { |
| 28 | $request->validate([ |
| 29 | 'email' => ['required', 'email'], |
| 30 | ]); |
| 31 | |
| 32 | // We will send the password reset link to this user. Once we have attempted |
| 33 | // to send the link, we will examine the response then see the message we |
| 34 | // need to show to the user. Finally, we'll send out a proper response. |
| 35 | $status = Password::sendResetLink( |
| 36 | $request->only('email') |
| 37 | ); |
| 38 | |
| 39 | return $status == Password::RESET_LINK_SENT |
| 40 | ? back()->with('status', __($status)) |
| 41 | : back()->withInput($request->only('email')) |
| 42 | ->withErrors(['email' => __($status)]); |
| 43 | } |
| 44 | } |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 14 |
+
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| RegisteredUserController | +
+
+
+ |
+ 0.00% |
+ 0 / 14 |
+
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+ 6 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| create | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| store | +
+
+
+ |
+ 0.00% |
+ 0 / 13 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Http\Controllers\Auth; |
| 4 | |
| 5 | use App\Http\Controllers\Controller; |
| 6 | use App\Models\User; |
| 7 | use Illuminate\Auth\Events\Registered; |
| 8 | use Illuminate\Http\RedirectResponse; |
| 9 | use Illuminate\Http\Request; |
| 10 | use Illuminate\Support\Facades\Auth; |
| 11 | use Illuminate\Support\Facades\Hash; |
| 12 | use Illuminate\Validation\Rules; |
| 13 | use Illuminate\View\View; |
| 14 | |
| 15 | class RegisteredUserController extends Controller |
| 16 | { |
| 17 | /** |
| 18 | * Display the registration view. |
| 19 | */ |
| 20 | public function create(): View |
| 21 | { |
| 22 | return view('auth.register'); |
| 23 | } |
| 24 | |
| 25 | /** |
| 26 | * Handle an incoming registration request. |
| 27 | * |
| 28 | * @throws \Illuminate\Validation\ValidationException |
| 29 | */ |
| 30 | public function store(Request $request): RedirectResponse |
| 31 | { |
| 32 | $request->validate([ |
| 33 | 'name' => ['required', 'string', 'max:255'], |
| 34 | 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class], |
| 35 | 'password' => ['required', 'confirmed', Rules\Password::defaults()], |
| 36 | ]); |
| 37 | |
| 38 | $user = User::create([ |
| 39 | 'name' => $request->name, |
| 40 | 'email' => $request->email, |
| 41 | 'password' => Hash::make($request->password), |
| 42 | ]); |
| 43 | |
| 44 | event(new Registered($user)); |
| 45 | |
| 46 | Auth::login($user); |
| 47 | |
| 48 | return redirect(route('dashboard', absolute: false)); |
| 49 | } |
| 50 | } |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 5 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| VerifyEmailController | +
+
+
+ |
+ 0.00% |
+ 0 / 5 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 12 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| __invoke | +
+
+
+ |
+ 0.00% |
+ 0 / 5 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 12 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Http\Controllers\Auth; |
| 4 | |
| 5 | use App\Http\Controllers\Controller; |
| 6 | use Illuminate\Auth\Events\Verified; |
| 7 | use Illuminate\Foundation\Auth\EmailVerificationRequest; |
| 8 | use Illuminate\Http\RedirectResponse; |
| 9 | |
| 10 | class VerifyEmailController extends Controller |
| 11 | { |
| 12 | /** |
| 13 | * Mark the authenticated user's email address as verified. |
| 14 | */ |
| 15 | public function __invoke(EmailVerificationRequest $request): RedirectResponse |
| 16 | { |
| 17 | if ($request->user()->hasVerifiedEmail()) { |
| 18 | return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); |
| 19 | } |
| 20 | |
| 21 | if ($request->user()->markEmailAsVerified()) { |
| 22 | event(new Verified($request->user())); |
| 23 | } |
| 24 | |
| 25 | return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); |
| 26 | } |
| 27 | } |
| + | Code Coverage |
+ ||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ ||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 83 |
+
+
+
+ |
+ 0.00% |
+ 0 / 15 |
+
+
+
+ |
+ 0.00% |
+ 0 / 9 |
+
|
+
+
+ |
+ 0.00% |
+ 0 / 8 |
+
+
+
+ |
+ 0.00% |
+ 0 / 3 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
|
+
+
+ |
+ 0.00% |
+ 0 / 10 |
+
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
|
+
+
+ |
+ 0.00% |
+ 0 / 4 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
|
+
+
+ |
+ 0.00% |
+ 0 / 3 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
|
+
+
+ |
+ 0.00% |
+ 0 / 20 |
+
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
|
+
+
+ |
+ 0.00% |
+ 0 / 8 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
|
+
+
+ |
+ 0.00% |
+ 0 / 11 |
+
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
|
+
+
+ |
+ 0.00% |
+ 0 / 14 |
+
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
|
+
+
+ |
+ 0.00% |
+ 0 / 5 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | ++ | n/a |
+ 0 / 0 |
+ + | n/a |
+ 0 / 0 |
+ CRAP | ++ | n/a |
+ 0 / 0 |
+
| Controller | ++ | n/a |
+ 0 / 0 |
+ + | n/a |
+ 0 / 0 |
+ 0 | ++ | n/a |
+ 0 / 0 |
+
| 1 | <?php |
| 2 | |
| 3 | namespace App\Http\Controllers; |
| 4 | |
| 5 | abstract class Controller |
| 6 | { |
| 7 | // |
| 8 | } |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 17 |
+
+
+
+ |
+ 0.00% |
+ 0 / 3 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| ProfileController | +
+
+
+ |
+ 0.00% |
+ 0 / 17 |
+
+
+
+ |
+ 0.00% |
+ 0 / 3 |
+ 20 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| edit | +
+
+
+ |
+ 0.00% |
+ 0 / 3 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| update | +
+
+
+ |
+ 0.00% |
+ 0 / 5 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 6 | ++ | ||
| destroy | +
+
+
+ |
+ 0.00% |
+ 0 / 9 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Http\Controllers; |
| 4 | |
| 5 | use App\Http\Requests\ProfileUpdateRequest; |
| 6 | use Illuminate\Http\RedirectResponse; |
| 7 | use Illuminate\Http\Request; |
| 8 | use Illuminate\Support\Facades\Auth; |
| 9 | use Illuminate\Support\Facades\Redirect; |
| 10 | use Illuminate\View\View; |
| 11 | |
| 12 | class ProfileController extends Controller |
| 13 | { |
| 14 | /** |
| 15 | * Display the user's profile form. |
| 16 | */ |
| 17 | public function edit(Request $request): View |
| 18 | { |
| 19 | return view('profile.edit', [ |
| 20 | 'user' => $request->user(), |
| 21 | ]); |
| 22 | } |
| 23 | |
| 24 | /** |
| 25 | * Update the user's profile information. |
| 26 | */ |
| 27 | public function update(ProfileUpdateRequest $request): RedirectResponse |
| 28 | { |
| 29 | $request->user()->fill($request->validated()); |
| 30 | |
| 31 | if ($request->user()->isDirty('email')) { |
| 32 | $request->user()->email_verified_at = null; |
| 33 | } |
| 34 | |
| 35 | $request->user()->save(); |
| 36 | |
| 37 | return Redirect::route('profile.edit')->with('status', 'profile-updated'); |
| 38 | } |
| 39 | |
| 40 | /** |
| 41 | * Delete the user's account. |
| 42 | */ |
| 43 | public function destroy(Request $request): RedirectResponse |
| 44 | { |
| 45 | $request->validateWithBag('userDeletion', [ |
| 46 | 'password' => ['required', 'current_password'], |
| 47 | ]); |
| 48 | |
| 49 | $user = $request->user(); |
| 50 | |
| 51 | Auth::logout(); |
| 52 | |
| 53 | $user->delete(); |
| 54 | |
| 55 | $request->session()->invalidate(); |
| 56 | $request->session()->regenerateToken(); |
| 57 | |
| 58 | return Redirect::to('/'); |
| 59 | } |
| 60 | } |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 5 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| TestLogController | +
+
+
+ |
+ 0.00% |
+ 0 / 5 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| __invoke | +
+
+
+ |
+ 0.00% |
+ 0 / 5 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Http\Controllers; |
| 4 | |
| 5 | use Illuminate\Http\Request; |
| 6 | use Illuminate\Support\Facades\Log; |
| 7 | |
| 8 | class TestLogController extends Controller |
| 9 | { |
| 10 | public function __invoke(Request $request) |
| 11 | { |
| 12 | Log::channel('telegram')->error('error level error'); |
| 13 | Log::channel('telegram')->critical('critical level error'); |
| 14 | Log::channel('telegram')->alert('alert level error'); |
| 15 | Log::channel('telegram')->emergency('emergency level error'); |
| 16 | |
| 17 | return "Логи отправлены. Проверьте свои Telegram каналы."; |
| 18 | } |
| 19 | } |
| Method | +Coverage | +
|---|---|
| __construct | 0% |
| index | 0% |
| create | 0% |
| store | 0% |
| edit | 0% |
| update | 0% |
| destroy | 0% |
| __construct | 0% |
| index | 0% |
| create | 0% |
| store | 0% |
| edit | 0% |
| update | 0% |
| destroy | 0% |
| create | 0% |
| store | 0% |
| destroy | 0% |
| show | 0% |
| store | 0% |
| store | 0% |
| __invoke | 0% |
| create | 0% |
| store | 0% |
| update | 0% |
| create | 0% |
| store | 0% |
| create | 0% |
| store | 0% |
| __invoke | 0% |
| edit | 0% |
| update | 0% |
| destroy | 0% |
| __invoke | 0% |
| + | Code Coverage |
+ ||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ ||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 219 |
+
+
+
+ |
+ 0.00% |
+ 0 / 33 |
+
+
+
+ |
+ 0.00% |
+ 0 / 13 |
+
|
+
+
+ |
+ 0.00% |
+ 0 / 114 |
+
+
+
+ |
+ 0.00% |
+ 0 / 14 |
+
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+ |
|
+
+
+ |
+ 0.00% |
+ 0 / 83 |
+
+
+
+ |
+ 0.00% |
+ 0 / 15 |
+
+
+
+ |
+ 0.00% |
+ 0 / 9 |
+ |
| + | n/a |
+ 0 / 0 |
+ + | n/a |
+ 0 / 0 |
+ + | n/a |
+ 0 / 0 |
+ |
|
+
+
+ |
+ 0.00% |
+ 0 / 17 |
+
+
+
+ |
+ 0.00% |
+ 0 / 3 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
|
+
+
+ |
+ 0.00% |
+ 0 / 5 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 5 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| SetLocale | +
+
+
+ |
+ 0.00% |
+ 0 / 5 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 6 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| handle | +
+
+
+ |
+ 0.00% |
+ 0 / 5 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 6 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Http\Middleware; |
| 4 | |
| 5 | use Closure; |
| 6 | use Illuminate\Http\Request; |
| 7 | use Symfony\Component\HttpFoundation\Response; |
| 8 | use Illuminate\Support\Facades\App; |
| 9 | |
| 10 | class SetLocale |
| 11 | { |
| 12 | /** |
| 13 | * Handle an incoming request. |
| 14 | * |
| 15 | * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next |
| 16 | */ |
| 17 | public function handle(Request $request, Closure $next): Response |
| 18 | { |
| 19 | $supportedLocales = config('app.supported_locales'); |
| 20 | $locale = $request->segment(1); |
| 21 | |
| 22 | if (in_array($locale, $supportedLocales)) { |
| 23 | App::setLocale($locale); |
| 24 | } |
| 25 | |
| 26 | return $next($request); |
| 27 | } |
| 28 | } |
| Class | +Coverage | +
|---|---|
| App\Http\Middleware\SetLocale | 0% |
| Class | +CRAP | +
|---|---|
| App\Http\Middleware\SetLocale | 6 |
| + | Code Coverage |
+ ||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ ||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 5 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
|
+
+
+ |
+ 0.00% |
+ 0 / 5 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 23 |
+
+
+
+ |
+ 0.00% |
+ 0 / 5 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| LoginRequest | +
+
+
+ |
+ 0.00% |
+ 0 / 23 |
+
+
+
+ |
+ 0.00% |
+ 0 / 5 |
+ 56 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| authorize | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| rules | +
+
+
+ |
+ 0.00% |
+ 0 / 4 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| authenticate | +
+
+
+ |
+ 0.00% |
+ 0 / 7 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 6 | ++ | ||
| ensureIsNotRateLimited | +
+
+
+ |
+ 0.00% |
+ 0 / 10 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 6 | ++ | ||
| throttleKey | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Http\Requests\Auth; |
| 4 | |
| 5 | use Illuminate\Auth\Events\Lockout; |
| 6 | use Illuminate\Foundation\Http\FormRequest; |
| 7 | use Illuminate\Support\Facades\Auth; |
| 8 | use Illuminate\Support\Facades\RateLimiter; |
| 9 | use Illuminate\Support\Str; |
| 10 | use Illuminate\Validation\ValidationException; |
| 11 | |
| 12 | class LoginRequest extends FormRequest |
| 13 | { |
| 14 | /** |
| 15 | * Determine if the user is authorized to make this request. |
| 16 | */ |
| 17 | public function authorize(): bool |
| 18 | { |
| 19 | return true; |
| 20 | } |
| 21 | |
| 22 | /** |
| 23 | * Get the validation rules that apply to the request. |
| 24 | * |
| 25 | * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string> |
| 26 | */ |
| 27 | public function rules(): array |
| 28 | { |
| 29 | return [ |
| 30 | 'email' => ['required', 'string', 'email'], |
| 31 | 'password' => ['required', 'string'], |
| 32 | ]; |
| 33 | } |
| 34 | |
| 35 | /** |
| 36 | * Attempt to authenticate the request's credentials. |
| 37 | * |
| 38 | * @throws \Illuminate\Validation\ValidationException |
| 39 | */ |
| 40 | public function authenticate(): void |
| 41 | { |
| 42 | $this->ensureIsNotRateLimited(); |
| 43 | |
| 44 | if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) { |
| 45 | RateLimiter::hit($this->throttleKey()); |
| 46 | |
| 47 | throw ValidationException::withMessages([ |
| 48 | 'email' => trans('auth.failed'), |
| 49 | ]); |
| 50 | } |
| 51 | |
| 52 | RateLimiter::clear($this->throttleKey()); |
| 53 | } |
| 54 | |
| 55 | /** |
| 56 | * Ensure the login request is not rate limited. |
| 57 | * |
| 58 | * @throws \Illuminate\Validation\ValidationException |
| 59 | */ |
| 60 | public function ensureIsNotRateLimited(): void |
| 61 | { |
| 62 | if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) { |
| 63 | return; |
| 64 | } |
| 65 | |
| 66 | event(new Lockout($this)); |
| 67 | |
| 68 | $seconds = RateLimiter::availableIn($this->throttleKey()); |
| 69 | |
| 70 | throw ValidationException::withMessages([ |
| 71 | 'email' => trans('auth.throttle', [ |
| 72 | 'seconds' => $seconds, |
| 73 | 'minutes' => ceil($seconds / 60), |
| 74 | ]), |
| 75 | ]); |
| 76 | } |
| 77 | |
| 78 | /** |
| 79 | * Get the rate limiting throttle key for the request. |
| 80 | */ |
| 81 | public function throttleKey(): string |
| 82 | { |
| 83 | return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip()); |
| 84 | } |
| 85 | } |
| Class | +Coverage | +
|---|---|
| App\Http\Requests\Auth\LoginRequest | 0% |
| Class | +CRAP | +
|---|---|
| App\Http\Requests\Auth\LoginRequest | 56 |
| Method | +Coverage | +
|---|---|
| authorize | 0% |
| rules | 0% |
| authenticate | 0% |
| ensureIsNotRateLimited | 0% |
| throttleKey | 0% |
| Method | +CRAP | +
|---|---|
| authenticate | 6 |
| ensureIsNotRateLimited | 6 |
| + | Code Coverage |
+ ||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ ||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 23 |
+
+
+
+ |
+ 0.00% |
+ 0 / 5 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
|
+
+
+ |
+ 0.00% |
+ 0 / 23 |
+
+
+
+ |
+ 0.00% |
+ 0 / 5 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 11 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| ProfileUpdateRequest | +
+
+
+ |
+ 0.00% |
+ 0 / 11 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| rules | +
+
+
+ |
+ 0.00% |
+ 0 / 11 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Http\Requests; |
| 4 | |
| 5 | use App\Models\User; |
| 6 | use Illuminate\Foundation\Http\FormRequest; |
| 7 | use Illuminate\Validation\Rule; |
| 8 | |
| 9 | class ProfileUpdateRequest extends FormRequest |
| 10 | { |
| 11 | /** |
| 12 | * Get the validation rules that apply to the request. |
| 13 | * |
| 14 | * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string> |
| 15 | */ |
| 16 | public function rules(): array |
| 17 | { |
| 18 | return [ |
| 19 | 'name' => ['required', 'string', 'max:255'], |
| 20 | 'email' => [ |
| 21 | 'required', |
| 22 | 'string', |
| 23 | 'lowercase', |
| 24 | 'email', |
| 25 | 'max:255', |
| 26 | Rule::unique(User::class)->ignore($this->user()->id), |
| 27 | ], |
| 28 | ]; |
| 29 | } |
| 30 | } |
| Class | +Coverage | +
|---|---|
| App\Http\Requests\Auth\LoginRequest | 0% |
| App\Http\Requests\ProfileUpdateRequest | 0% |
| Class | +CRAP | +
|---|---|
| App\Http\Requests\Auth\LoginRequest | 56 |
| Method | +Coverage | +
|---|---|
| authorize | 0% |
| rules | 0% |
| authenticate | 0% |
| ensureIsNotRateLimited | 0% |
| throttleKey | 0% |
| rules | 0% |
| Method | +CRAP | +
|---|---|
| authenticate | 6 |
| ensureIsNotRateLimited | 6 |
| + | Code Coverage |
+ ||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ ||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 34 |
+
+
+
+ |
+ 0.00% |
+ 0 / 6 |
+
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+
|
+
+
+ |
+ 0.00% |
+ 0 / 23 |
+
+
+
+ |
+ 0.00% |
+ 0 / 5 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
|
+
+
+ |
+ 0.00% |
+ 0 / 11 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 100.00% |
+ 19 / 19 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ CRAP | +
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+
| CategoryCollection | +
+
+
+ |
+ 100.00% |
+ 19 / 19 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 1 | +
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+
| toArray | +
+
+
+ |
+ 100.00% |
+ 19 / 19 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 1 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Http\Resources; |
| 4 | |
| 5 | use Illuminate\Http\Resources\Json\ResourceCollection; |
| 6 | |
| 7 | /** |
| 8 | * @OA\Schema( |
| 9 | * schema="CategoryCollection", |
| 10 | * type="object", |
| 11 | * title="Paginated Category List", |
| 12 | * description="Collection of categories with pagination metadata", |
| 13 | * @OA\Property( |
| 14 | * property="data", |
| 15 | * type="array", |
| 16 | * description="Array of category items", |
| 17 | * @OA\Items(ref="#/components/schemas/Category") |
| 18 | * ), |
| 19 | * @OA\Property( |
| 20 | * property="links", |
| 21 | * type="object", |
| 22 | * description="Pagination links", |
| 23 | * @OA\Property( |
| 24 | * property="first", |
| 25 | * type="string", |
| 26 | * format="url", |
| 27 | * example="http://api.example.com/v1/categories?page=1" |
| 28 | * ), |
| 29 | * @OA\Property( |
| 30 | * property="last", |
| 31 | * type="string", |
| 32 | * format="url", |
| 33 | * example="http://api.example.com/v1/categories?page=10" |
| 34 | * ), |
| 35 | * @OA\Property( |
| 36 | * property="prev", |
| 37 | * type="string", |
| 38 | * format="url", |
| 39 | * nullable=true, |
| 40 | * example="http://api.example.com/v1/categories?page=1" |
| 41 | * ), |
| 42 | * @OA\Property( |
| 43 | * property="next", |
| 44 | * type="string", |
| 45 | * format="url", |
| 46 | * nullable=true, |
| 47 | * example="http://api.example.com/v1/categories?page=3" |
| 48 | * ), |
| 49 | * @OA\Property( |
| 50 | * property="self", |
| 51 | * type="string", |
| 52 | * format="url", |
| 53 | * example="http://api.example.com/v1/categories?page=2" |
| 54 | * ) |
| 55 | * ), |
| 56 | * @OA\Property( |
| 57 | * property="meta", |
| 58 | * type="object", |
| 59 | * description="Pagination metadata", |
| 60 | * @OA\Property( |
| 61 | * property="current_page", |
| 62 | * type="integer", |
| 63 | * example=2 |
| 64 | * ), |
| 65 | * @OA\Property( |
| 66 | * property="from", |
| 67 | * type="integer", |
| 68 | * example=16 |
| 69 | * ), |
| 70 | * @OA\Property( |
| 71 | * property="last_page", |
| 72 | * type="integer", |
| 73 | * example=10 |
| 74 | * ), |
| 75 | * @OA\Property( |
| 76 | * property="path", |
| 77 | * type="string", |
| 78 | * format="url", |
| 79 | * example="http://api.example.com/v1/categories" |
| 80 | * ), |
| 81 | * @OA\Property( |
| 82 | * property="per_page", |
| 83 | * type="integer", |
| 84 | * example=15 |
| 85 | * ), |
| 86 | * @OA\Property( |
| 87 | * property="to", |
| 88 | * type="integer", |
| 89 | * example=30 |
| 90 | * ), |
| 91 | * @OA\Property( |
| 92 | * property="total", |
| 93 | * type="integer", |
| 94 | * example=150 |
| 95 | * ), |
| 96 | * @OA\Property( |
| 97 | * property="version", |
| 98 | * type="string", |
| 99 | * example="1.0.0" |
| 100 | * ), |
| 101 | * @OA\Property( |
| 102 | * property="timestamp", |
| 103 | * type="string", |
| 104 | * format="date-time", |
| 105 | * example="2023-05-15T12:34:56Z" |
| 106 | * ) |
| 107 | * ) |
| 108 | * ) |
| 109 | */ |
| 110 | class CategoryCollection extends ResourceCollection |
| 111 | { |
| 112 | /** |
| 113 | * The "data" wrapper that should be applied. |
| 114 | * |
| 115 | * @var string |
| 116 | */ |
| 117 | public static $wrap = 'data'; |
| 118 | |
| 119 | /** |
| 120 | * Transform the resource collection into an array. |
| 121 | * |
| 122 | * @param \Illuminate\Http\Request $request |
| 123 | * @return array |
| 124 | */ |
| 125 | public function toArray($request): array |
| 126 | { |
| 127 | return [ |
| 128 | 'data' => $this->collection, |
| 129 | 'links' => [ |
| 130 | 'first' => $this->url(1), |
| 131 | 'last' => $this->url($this->lastPage()), |
| 132 | 'prev' => $this->previousPageUrl(), |
| 133 | 'next' => $this->nextPageUrl(), |
| 134 | 'self' => $request->fullUrl(), |
| 135 | ], |
| 136 | 'meta' => [ |
| 137 | 'current_page' => $this->currentPage(), |
| 138 | 'from' => $this->firstItem(), |
| 139 | 'last_page' => $this->lastPage(), |
| 140 | 'path' => $this->path(), |
| 141 | 'per_page' => $this->perPage(), |
| 142 | 'to' => $this->lastItem(), |
| 143 | 'total' => $this->total() |
| 144 | ] |
| 145 | ]; |
| 146 | } |
| 147 | |
| 148 | } |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 100.00% |
+ 14 / 14 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ CRAP | +
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+
| CategoryResource | +
+
+
+ |
+ 100.00% |
+ 14 / 14 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 1 | +
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+
| toArray | +
+
+
+ |
+ 100.00% |
+ 14 / 14 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 1 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Http\Resources; |
| 4 | |
| 5 | use Illuminate\Http\Request; |
| 6 | use Illuminate\Http\Resources\Json\JsonResource; |
| 7 | use App\Models\Category; |
| 8 | |
| 9 | /** |
| 10 | * @OA\Schema( |
| 11 | * schema="Category", |
| 12 | * type="object", |
| 13 | * title="Category", |
| 14 | * description="Product category model", |
| 15 | * @OA\Property(property="id", type="integer", example=1), |
| 16 | * @OA\Property(property="title", type="string", example="Electronics"), |
| 17 | * @OA\Property(property="alias", type="string", example="electronics"), |
| 18 | * @OA\Property(property="text", type="string", nullable=true, example="All electronic devices"), |
| 19 | * @OA\Property(property="published", type="boolean", example=true), |
| 20 | * @OA\Property(property="order", type="integer", example=1), |
| 21 | * @OA\Property(property="created_at", type="string", format="date-time"), |
| 22 | * @OA\Property(property="updated_at", type="string", format="date-time"), |
| 23 | * @OA\Property( |
| 24 | * property="products", |
| 25 | * type="array", |
| 26 | * @OA\Items(ref="#/components/schemas/Product") |
| 27 | * ), |
| 28 | * @OA\Property( |
| 29 | * property="meta", |
| 30 | * type="object", |
| 31 | * @OA\Property(property="checked_at", type="string", format="date-time"), |
| 32 | * @OA\Property(property="version", type="string", example="1.0") |
| 33 | * ) |
| 34 | * ) |
| 35 | */ |
| 36 | class CategoryResource extends JsonResource |
| 37 | { |
| 38 | /** |
| 39 | * Transform the resource into an array. |
| 40 | * |
| 41 | * @return array<string, mixed> |
| 42 | */ |
| 43 | public function toArray(Request $request): array |
| 44 | { |
| 45 | /** @var Category $category */ |
| 46 | $category = $this->resource; |
| 47 | |
| 48 | return [ |
| 49 | 'id' => $category->id, |
| 50 | 'title' => $category->title, |
| 51 | 'alias' => $category->alias, |
| 52 | 'text' => $category->text, |
| 53 | 'published' => (bool)$category->published, |
| 54 | 'order' => $category->order, |
| 55 | 'created_at' => $category->created_at?->toIso8601String(), |
| 56 | 'updated_at' => $category->updated_at?->toIso8601String(), |
| 57 | 'products' => $this->whenLoaded('products', function () { |
| 58 | return ProductResource::collection($this->products); |
| 59 | }), |
| 60 | ]; |
| 61 | } |
| 62 | } |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 100.00% |
+ 13 / 13 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ CRAP | +
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+
| ProductCollection | +
+
+
+ |
+ 100.00% |
+ 13 / 13 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 1 | +
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+
| toArray | +
+
+
+ |
+ 100.00% |
+ 13 / 13 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 1 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Http\Resources; |
| 4 | |
| 5 | use Illuminate\Http\Resources\Json\ResourceCollection; |
| 6 | |
| 7 | class ProductCollection extends ResourceCollection |
| 8 | { |
| 9 | /** |
| 10 | * Transform the resource collection into an array. |
| 11 | * |
| 12 | * @param \Illuminate\Http\Request $request |
| 13 | * @return array |
| 14 | */ |
| 15 | public function toArray($request): array |
| 16 | { |
| 17 | return [ |
| 18 | 'data' => $this->collection, |
| 19 | 'links' => [ |
| 20 | 'self' => $request->fullUrl(), |
| 21 | ], |
| 22 | 'meta' => [ |
| 23 | 'count' => $this->count(), |
| 24 | 'total' => $this->total(), |
| 25 | 'current_page' => $this->currentPage(), |
| 26 | 'last_page' => $this->lastPage(), |
| 27 | 'per_page' => $this->perPage(), |
| 28 | ] |
| 29 | ]; |
| 30 | } |
| 31 | } |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 100.00% |
+ 18 / 18 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ CRAP | +
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+
| ProductResource | +
+
+
+ |
+ 100.00% |
+ 18 / 18 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 2 | +
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+
| toArray | +
+
+
+ |
+ 100.00% |
+ 18 / 18 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 2 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Http\Resources; |
| 4 | |
| 5 | use Illuminate\Http\Request; |
| 6 | use Illuminate\Http\Resources\Json\JsonResource; |
| 7 | use App\Models\Product; |
| 8 | |
| 9 | /** |
| 10 | * @OA\Schema( |
| 11 | * schema="Product", |
| 12 | * type="object", |
| 13 | * title="Product", |
| 14 | * description="Product model", |
| 15 | * @OA\Property(property="id", type="integer", example=1), |
| 16 | * @OA\Property(property="title", type="string", example="Premium Headphones"), |
| 17 | * @OA\Property(property="alias", type="string", example="premium-headphones"), |
| 18 | * @OA\Property(property="text", type="string", nullable=true, example="High quality headphones with noise cancellation"), |
| 19 | * @OA\Property(property="image", type="string", nullable=true, example="headphones.jpg"), |
| 20 | * @OA\Property( |
| 21 | * property="images", |
| 22 | * type="array", |
| 23 | * nullable=true, |
| 24 | * @OA\Items(type="string", example="image1.jpg") |
| 25 | * ), |
| 26 | * @OA\Property(property="is_sale", type="boolean", example=true), |
| 27 | * @OA\Property(property="published", type="boolean", example=true), |
| 28 | * @OA\Property(property="order", type="integer", example=1), |
| 29 | * @OA\Property(property="price", type="number", format="float", example=199.99), |
| 30 | * @OA\Property(property="created_at", type="string", format="date-time"), |
| 31 | * @OA\Property(property="updated_at", type="string", format="date-time"), |
| 32 | * @OA\Property( |
| 33 | * property="categories", |
| 34 | * type="array", |
| 35 | * @OA\Items(ref="#/components/schemas/Category") |
| 36 | * ), |
| 37 | * @OA\Property( |
| 38 | * property="meta", |
| 39 | * type="object", |
| 40 | * @OA\Property(property="checked_at", type="string", format="date-time"), |
| 41 | * @OA\Property(property="version", type="string", example="1.0") |
| 42 | * ) |
| 43 | * ) |
| 44 | */ |
| 45 | class ProductResource extends JsonResource |
| 46 | { |
| 47 | /** |
| 48 | * Transform the resource into an array. |
| 49 | * |
| 50 | * @return array<string, mixed> |
| 51 | */ |
| 52 | public function toArray(Request $request): array |
| 53 | { |
| 54 | /** @var Product $product */ |
| 55 | $product = $this->resource; |
| 56 | |
| 57 | return [ |
| 58 | 'id' => $product->id, |
| 59 | 'title' => $product->title, |
| 60 | 'alias' => $product->alias, |
| 61 | 'text' => $product->text, |
| 62 | 'image' => $product->image, |
| 63 | 'images' => $product->images ? json_decode($product->images, true) : null, |
| 64 | 'is_sale' => (bool)$product->is_sale, |
| 65 | 'published' => (bool)$product->published, |
| 66 | 'order' => $product->order, |
| 67 | 'price' => (float)$product->price, |
| 68 | 'created_at' => $product->created_at?->toIso8601String(), |
| 69 | 'updated_at' => $product->updated_at?->toIso8601String(), |
| 70 | 'categories' => $this->whenLoaded('categories', function () use ($product) { |
| 71 | return CategoryResource::collection($product->categories); |
| 72 | }), |
| 73 | ]; |
| 74 | } |
| 75 | |
| 76 | /* |
| 77 | public function with(Request $request): array |
| 78 | { |
| 79 | return [ |
| 80 | 'meta' => [ |
| 81 | 'checked_at' => new \DateTime(), |
| 82 | ] |
| 83 | ]; |
| 84 | }*/ |
| 85 | } |
| Class | +Coverage | +
|---|
| Class | +CRAP | +
|---|
| Method | +Coverage | +
|---|
| Method | +CRAP | +
|---|
| + | Code Coverage |
+ ||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ ||||||
| Total | +
+
+
+ |
+ 100.00% |
+ 64 / 64 |
+
+
+
+ |
+ 100.00% |
+ 4 / 4 |
+
+
+
+ |
+ 100.00% |
+ 4 / 4 |
+
|
+
+
+ |
+ 100.00% |
+ 19 / 19 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ |
|
+
+
+ |
+ 100.00% |
+ 14 / 14 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ |
|
+
+
+ |
+ 100.00% |
+ 13 / 13 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ |
|
+
+
+ |
+ 100.00% |
+ 18 / 18 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ |
| Method | +Coverage | +
|---|---|
| __construct | 0% |
| index | 0% |
| create | 0% |
| store | 0% |
| edit | 0% |
| update | 0% |
| destroy | 0% |
| __construct | 0% |
| index | 0% |
| create | 0% |
| store | 0% |
| edit | 0% |
| update | 0% |
| destroy | 0% |
| create | 0% |
| store | 0% |
| destroy | 0% |
| show | 0% |
| store | 0% |
| store | 0% |
| __invoke | 0% |
| create | 0% |
| store | 0% |
| update | 0% |
| create | 0% |
| store | 0% |
| create | 0% |
| store | 0% |
| __invoke | 0% |
| edit | 0% |
| update | 0% |
| destroy | 0% |
| __invoke | 0% |
| handle | 0% |
| authorize | 0% |
| rules | 0% |
| authenticate | 0% |
| ensureIsNotRateLimited | 0% |
| throttleKey | 0% |
| rules | 0% |
| show | 40% |
| update | 83% |
| index | 88% |
| + | Code Coverage |
+ ||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ ||||||
| Total | +
+
+
+ |
+ 52.88% |
+ 303 / 573 |
+
+
+
+ |
+ 31.82% |
+ 21 / 66 |
+
+
+
+ |
+ 20.83% |
+ 5 / 24 |
+
|
+
+
+ |
+ 95.22% |
+ 239 / 251 |
+
+
+
+ |
+ 77.27% |
+ 17 / 22 |
+
+
+
+ |
+ 25.00% |
+ 1 / 4 |
+ |
|
+
+
+ |
+ 0.00% |
+ 0 / 219 |
+
+
+
+ |
+ 0.00% |
+ 0 / 33 |
+
+
+
+ |
+ 0.00% |
+ 0 / 13 |
+ |
|
+
+
+ |
+ 0.00% |
+ 0 / 5 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
|
+
+
+ |
+ 0.00% |
+ 0 / 34 |
+
+
+
+ |
+ 0.00% |
+ 0 / 6 |
+
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+ |
|
+
+
+ |
+ 100.00% |
+ 64 / 64 |
+
+
+
+ |
+ 100.00% |
+ 4 / 4 |
+
+
+
+ |
+ 100.00% |
+ 4 / 4 |
+ |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 36 |
+
+
+
+ |
+ 0.00% |
+ 0 / 3 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| SendPriceChangeNotification | +
+
+
+ |
+ 0.00% |
+ 0 / 36 |
+
+
+
+ |
+ 0.00% |
+ 0 / 3 |
+ 30 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| __construct | +
+
+
+ |
+ 0.00% |
+ 0 / 4 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| handle | +
+
+
+ |
+ 0.00% |
+ 0 / 28 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 12 | ++ | ||
| failed | +
+
+
+ |
+ 0.00% |
+ 0 / 4 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Jobs; |
| 4 | |
| 5 | use App\DTO\ProductPriceData; |
| 6 | use App\Models\User; |
| 7 | use App\Notifications\ProductPriceChangedNotification; |
| 8 | use Illuminate\Bus\Queueable; |
| 9 | use Illuminate\Contracts\Queue\ShouldQueue; |
| 10 | use Illuminate\Foundation\Bus\Dispatchable; |
| 11 | use Illuminate\Queue\InteractsWithQueue; |
| 12 | use Illuminate\Queue\SerializesModels; |
| 13 | use Illuminate\Support\Facades\Log; |
| 14 | use App\Repositories\Contracts\ProductRepositoryInterface; |
| 15 | |
| 16 | class SendPriceChangeNotification implements ShouldQueue |
| 17 | { |
| 18 | use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; |
| 19 | |
| 20 | public int $productId; |
| 21 | public float $oldPrice; |
| 22 | public float $newPrice; |
| 23 | protected ProductRepositoryInterface $productRepository; |
| 24 | |
| 25 | /** |
| 26 | * Create a new job instance. |
| 27 | */ |
| 28 | public function __construct(ProductPriceData $priceData,) |
| 29 | { |
| 30 | $this->productId = $priceData->productId; |
| 31 | $this->oldPrice = $priceData->oldPrice; |
| 32 | $this->newPrice = $priceData->newPrice; |
| 33 | $this->productRepository = app(ProductRepositoryInterface::class); |
| 34 | } |
| 35 | |
| 36 | /** |
| 37 | * Execute the job. |
| 38 | */ |
| 39 | public function handle(): void |
| 40 | { |
| 41 | try { |
| 42 | // Получаем всех пользователей, которым нужно отправить уведомление |
| 43 | // Можно добавить дополнительную логику для фильтрации пользователей |
| 44 | $users = User::where('email_verified_at', '!=', null) |
| 45 | ->whereHas('notifications_settings', function($query) { |
| 46 | $query->where('price_changes', true); |
| 47 | }) |
| 48 | ->orWhereDoesntHave('notifications_settings') |
| 49 | ->get(); |
| 50 | |
| 51 | $product = $this->productRepository->find($this->productId);; |
| 52 | |
| 53 | foreach ($users as $user) { |
| 54 | $user->notify(new ProductPriceChangedNotification( |
| 55 | $this->productId, |
| 56 | $this->oldPrice, |
| 57 | $this->newPrice, |
| 58 | $product->title, |
| 59 | $product->alias |
| 60 | )); |
| 61 | } |
| 62 | |
| 63 | |
| 64 | Log::info('Price change notifications sent', [ |
| 65 | 'product_id' => $this->productId, |
| 66 | 'product_title' => $product->title, |
| 67 | 'old_price' => $this->oldPrice, |
| 68 | 'new_price' => $this->newPrice, |
| 69 | 'users_notified' => $users->count() |
| 70 | ]); |
| 71 | |
| 72 | } catch (\Exception $e) { |
| 73 | Log::error('Failed to send price change notifications', [ |
| 74 | 'product_id' => $this->productId, |
| 75 | 'error' => $e->getMessage() |
| 76 | ]); |
| 77 | |
| 78 | throw $e; |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | /** |
| 83 | * Handle a job failure. |
| 84 | */ |
| 85 | public function failed(\Throwable $exception): void |
| 86 | { |
| 87 | Log::error('Price change notification job failed', [ |
| 88 | 'product_id' => $this->productId, |
| 89 | 'error' => $exception->getMessage() |
| 90 | ]); |
| 91 | } |
| 92 | } |
| Class | +Coverage | +
|---|---|
| App\Jobs\SendPriceChangeNotification | 0% |
| Class | +CRAP | +
|---|---|
| App\Jobs\SendPriceChangeNotification | 30 |
| Method | +Coverage | +
|---|---|
| __construct | 0% |
| handle | 0% |
| failed | 0% |
| Method | +CRAP | +
|---|---|
| handle | 12 |
| + | Code Coverage |
+ ||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ ||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 36 |
+
+
+
+ |
+ 0.00% |
+ 0 / 3 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
|
+
+
+ |
+ 0.00% |
+ 0 / 36 |
+
+
+
+ |
+ 0.00% |
+ 0 / 3 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 4 |
+
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| HandleProductPriceChange | +
+
+
+ |
+ 0.00% |
+ 0 / 4 |
+
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+ 6 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| __construct | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| handle | +
+
+
+ |
+ 0.00% |
+ 0 / 3 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Listeners; |
| 4 | |
| 5 | use App\Events\ProductPriceChanged; |
| 6 | use App\Jobs\SendPriceChangeNotification; |
| 7 | use Illuminate\Contracts\Queue\ShouldQueue; |
| 8 | use Illuminate\Queue\InteractsWithQueue; |
| 9 | |
| 10 | class HandleProductPriceChange implements ShouldQueue |
| 11 | { |
| 12 | use InteractsWithQueue; |
| 13 | |
| 14 | /** |
| 15 | * Create the event listener. |
| 16 | */ |
| 17 | public function __construct() |
| 18 | { |
| 19 | // |
| 20 | } |
| 21 | |
| 22 | /** |
| 23 | * Handle the event. |
| 24 | */ |
| 25 | public function handle(ProductPriceChanged $event): void |
| 26 | { |
| 27 | // Отправляем job в очередь для рассылки уведомлений |
| 28 | SendPriceChangeNotification::dispatch( |
| 29 | $event->productPriceData |
| 30 | )->delay(now()->addMinutes(1)); // Задержка в 1 минуту для избежания спама |
| 31 | } |
| 32 | } |
| Class | +Coverage | +
|---|---|
| App\Listeners\HandleProductPriceChange | 0% |
| Class | +CRAP | +
|---|
| Method | +Coverage | +
|---|---|
| __construct | 0% |
| handle | 0% |
| Method | +CRAP | +
|---|
| + | Code Coverage |
+ ||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ ||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 4 |
+
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
|
+
+
+ |
+ 0.00% |
+ 0 / 4 |
+
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 47 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| TelegramLogger | +
+
+
+ |
+ 0.00% |
+ 0 / 47 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 6 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| __invoke | +
+
+
+ |
+ 0.00% |
+ 0 / 47 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 6 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Logging; |
| 4 | |
| 5 | use Monolog\Handler\FilterHandler; |
| 6 | use Monolog\Level; |
| 7 | use Monolog\Logger; |
| 8 | use Monolog\Handler\TelegramBotHandler; |
| 9 | use Monolog\Handler\StreamHandler; |
| 10 | use Monolog\Handler\FallbackGroupHandler; |
| 11 | use Monolog\Formatter\LineFormatter; |
| 12 | |
| 13 | class TelegramLogger |
| 14 | { |
| 15 | public function __invoke(array $config) |
| 16 | { |
| 17 | $logger = new Logger('telegram'); |
| 18 | |
| 19 | // Настройки для разных уровней логирования |
| 20 | $levels = [ |
| 21 | 'error' => [ |
| 22 | 'chat_id' => env('TELEGRAM_ERROR_CHAT_ID'), |
| 23 | 'emoji' => '⚠️ ERROR', |
| 24 | 'level' => Level::Error, |
| 25 | ], |
| 26 | 'critical' => [ |
| 27 | 'chat_id' => env('TELEGRAM_CRITICAL_CHAT_ID'), |
| 28 | 'emoji' => '🔥 CRITICAL', |
| 29 | 'level' => Level::Critical, |
| 30 | ], |
| 31 | 'alert' => [ |
| 32 | 'chat_id' => env('TELEGRAM_ALERT_CHAT_ID'), |
| 33 | 'emoji' => '🚨 ALERT', |
| 34 | 'level' => Level::Alert, |
| 35 | ], |
| 36 | 'emergency' => [ |
| 37 | 'chat_id' => env('TELEGRAM_EMERGENCY_CHAT_ID'), |
| 38 | 'emoji' => '💥 EMERGENCY', |
| 39 | 'level' => Level::Emergency, |
| 40 | ], |
| 41 | ]; |
| 42 | |
| 43 | foreach ($levels as $levelName => $settings) { |
| 44 | // Telegram Handler (основной) |
| 45 | $telegramHandler = new TelegramBotHandler( |
| 46 | env('TELEGRAM_BOT_TOKEN'), |
| 47 | $settings['chat_id'], |
| 48 | $settings['level'] |
| 49 | ); |
| 50 | $telegramHandler->setFormatter( |
| 51 | new LineFormatter("{$settings['emoji']}: %message% %context% %extra%\n") |
| 52 | ); |
| 53 | |
| 54 | // File Handler (резервный) |
| 55 | $fileHandler = new StreamHandler( |
| 56 | storage_path("logs/telegram_{$levelName}.log"), |
| 57 | $settings['level'] |
| 58 | ); |
| 59 | $fileHandler->setFormatter( |
| 60 | new LineFormatter("[%datetime%] %level_name%: %message% %context% %extra%\n") |
| 61 | ); |
| 62 | |
| 63 | // Группа с fallback: сначала Telegram, потом файл |
| 64 | $fallbackGroup = new FallbackGroupHandler([$telegramHandler, $fileHandler]); |
| 65 | |
| 66 | $filterHandler = new FilterHandler( |
| 67 | $fallbackGroup, |
| 68 | $settings['level'], |
| 69 | $settings['level'] |
| 70 | ); |
| 71 | |
| 72 | $logger->pushHandler($filterHandler); |
| 73 | |
| 74 | |
| 75 | } |
| 76 | |
| 77 | return $logger; |
| 78 | } |
| 79 | |
| 80 | } |
| Class | +Coverage | +
|---|---|
| App\Logging\TelegramLogger | 0% |
| Class | +CRAP | +
|---|---|
| App\Logging\TelegramLogger | 6 |
| + | Code Coverage |
+ ||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ ||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 47 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
|
+
+
+ |
+ 0.00% |
+ 0 / 47 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | ++ | n/a |
+ 0 / 0 |
+ + | n/a |
+ 0 / 0 |
+ CRAP | ++ | n/a |
+ 0 / 0 |
+
| BaseModel | ++ | n/a |
+ 0 / 0 |
+ + | n/a |
+ 0 / 0 |
+ 0 | ++ | n/a |
+ 0 / 0 |
+
| 1 | <?php |
| 2 | |
| 3 | namespace App\Models; |
| 4 | |
| 5 | use Illuminate\Database\Eloquent\Model; |
| 6 | |
| 7 | class BaseModel extends Model |
| 8 | { |
| 9 | |
| 10 | } |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ CRAP | +
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+
| Category | +
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 1 | +
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+
| products | +
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 1 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Models; |
| 4 | |
| 5 | use Illuminate\Database\Eloquent\Factories\HasFactory; |
| 6 | |
| 7 | /** |
| 8 | * @class Category |
| 9 | * |
| 10 | * @property int $id |
| 11 | * @property string $title |
| 12 | * @property string $alias |
| 13 | * @property string|null $text |
| 14 | * @property int $published // Stored as tinyInteger, accessed as bool |
| 15 | * @property int $order |
| 16 | * @property int $user_id |
| 17 | * @property \Illuminate\Support\Carbon|null $created_at |
| 18 | * @property \Illuminate\Support\Carbon|null $updated_at |
| 19 | */ |
| 20 | |
| 21 | class Category extends BaseModel |
| 22 | { |
| 23 | /** @use HasFactory<\Database\Factories\CategoryFactory> */ |
| 24 | use HasFactory; |
| 25 | |
| 26 | protected $fillable = [ |
| 27 | 'title', |
| 28 | 'alias', |
| 29 | 'text', |
| 30 | 'published' |
| 31 | ]; |
| 32 | |
| 33 | public function products() |
| 34 | { |
| 35 | return $this->belongsToMany(Product::class); |
| 36 | } |
| 37 | } |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| NotificationSettings | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| user | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Models; |
| 4 | |
| 5 | use Illuminate\Database\Eloquent\Factories\HasFactory; |
| 6 | use Illuminate\Database\Eloquent\Model; |
| 7 | |
| 8 | /** |
| 9 | * @class NotificationSettings |
| 10 | * @extends Model |
| 11 | * |
| 12 | * @property int $id |
| 13 | * @property int $user_id |
| 14 | * @property bool $price_changes |
| 15 | * @property bool $new_products |
| 16 | * @property bool $sales |
| 17 | * @property \Illuminate\Support\Carbon|null $created_at |
| 18 | * @property \Illuminate\Support\Carbon|null $updated_at |
| 19 | * |
| 20 | * @property-read \App\Models\User $user |
| 21 | * |
| 22 | * This model represents the notification settings for a specific user. |
| 23 | * It allows users to customize which types of notifications they want to receive. |
| 24 | */ |
| 25 | class NotificationSettings extends Model |
| 26 | { |
| 27 | use HasFactory; |
| 28 | |
| 29 | protected $fillable = [ |
| 30 | 'user_id', |
| 31 | 'price_changes', |
| 32 | 'new_products', |
| 33 | 'sales', |
| 34 | ]; |
| 35 | |
| 36 | protected $casts = [ |
| 37 | 'price_changes' => 'boolean', |
| 38 | 'new_products' => 'boolean', |
| 39 | 'sales' => 'boolean', |
| 40 | ]; |
| 41 | |
| 42 | /** |
| 43 | * Get the user that owns the notification settings. |
| 44 | */ |
| 45 | public function user() |
| 46 | { |
| 47 | return $this->belongsTo(User::class); |
| 48 | } |
| 49 | } |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| Permission | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| roles | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Models; |
| 4 | |
| 5 | use Illuminate\Database\Eloquent\Factories\HasFactory; |
| 6 | use Illuminate\Database\Eloquent\Model; |
| 7 | |
| 8 | /** |
| 9 | * @class Permission |
| 10 | * |
| 11 | * @property int $id |
| 12 | * @property string $name |
| 13 | * @property string|null $description |
| 14 | * @property \Illuminate\Support\Carbon|null $created_at |
| 15 | * @property \Illuminate\Support\Carbon|null $updated_at |
| 16 | */ |
| 17 | class Permission extends Model |
| 18 | { |
| 19 | use HasFactory; |
| 20 | |
| 21 | protected $fillable = ['name', 'description']; |
| 22 | |
| 23 | /** |
| 24 | * Разрешение может принадлежать многим ролям. |
| 25 | */ |
| 26 | public function roles() |
| 27 | { |
| 28 | return $this->belongsToMany(Role::class); |
| 29 | } |
| 30 | } |
| 31 |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ CRAP | +
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+
| Product | +
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 1 | +
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+
| categories | +
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 1 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Models; |
| 4 | |
| 5 | use Illuminate\Database\Eloquent\Factories\HasFactory; |
| 6 | |
| 7 | /** |
| 8 | * @class Product |
| 9 | * |
| 10 | * @property int $id |
| 11 | * @property string $title |
| 12 | * @property string $alias |
| 13 | * @property string|null $text |
| 14 | * @property string|null $image |
| 15 | * @property string|null $images // Stored as JSON string in DB, accessed as array |
| 16 | * @property int $is_sale // Stored as tinyInteger, accessed as bool |
| 17 | * @property int $published // Stored as tinyInteger, accessed as bool |
| 18 | * @property int $order |
| 19 | * @property float $price |
| 20 | * @property int $user_id |
| 21 | * @property \Illuminate\Support\Carbon|null $created_at |
| 22 | * @property \Illuminate\Support\Carbon|null $updated_at |
| 23 | */ |
| 24 | class Product extends BaseModel |
| 25 | { |
| 26 | /** @use HasFactory<\Database\Factories\ProductFactory> */ |
| 27 | use HasFactory; |
| 28 | |
| 29 | /** |
| 30 | * Temporary storage for price change data |
| 31 | * |
| 32 | * @var array|null |
| 33 | */ |
| 34 | private $priceChangeData = null; |
| 35 | |
| 36 | |
| 37 | protected $fillable = [ |
| 38 | 'title', |
| 39 | 'alias', |
| 40 | 'text', |
| 41 | 'image', |
| 42 | 'images', |
| 43 | 'is_sale', |
| 44 | 'published', |
| 45 | 'order', |
| 46 | 'price', |
| 47 | ]; |
| 48 | |
| 49 | |
| 50 | public function categories(){ |
| 51 | return $this->belongsToMany(Category::class); |
| 52 | } |
| 53 | |
| 54 | } |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 33.33% |
+ 1 / 3 |
+
+
+
+ |
+ 33.33% |
+ 1 / 3 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| RefreshToken | +
+
+
+ |
+ 33.33% |
+ 1 / 3 |
+
+
+
+ |
+ 33.33% |
+ 1 / 3 |
+ 5.67 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| user | +
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 1 | ++ | ||
| isExpired | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| scopeValid | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Models; |
| 4 | |
| 5 | use Illuminate\Database\Eloquent\Factories\HasFactory; |
| 6 | use Illuminate\Database\Eloquent\Model; |
| 7 | use Illuminate\Database\Eloquent\Relations\BelongsTo; |
| 8 | use Illuminate\Database\Eloquent\Builder; |
| 9 | use Illuminate\Support\Carbon; |
| 10 | |
| 11 | /** |
| 12 | * Class RefreshToken |
| 13 | * |
| 14 | * @package App\Models |
| 15 | * |
| 16 | * @property int $id Идентификатор токена. |
| 17 | * @property int $user_id Идентификатор пользователя, которому принадлежит токен. |
| 18 | * @property string $token Уникальная строка токена. |
| 19 | * @property Carbon $expires_at Дата и время истечения срока действия токена. |
| 20 | * @property Carbon|null $created_at Дата и время создания токена. |
| 21 | * @property Carbon|null $updated_at Дата и время последнего обновления токена. |
| 22 | * |
| 23 | * @mixin Builder |
| 24 | */ |
| 25 | class RefreshToken extends Model |
| 26 | { |
| 27 | use HasFactory; |
| 28 | |
| 29 | /** |
| 30 | * Атрибуты, которые можно массово присваивать. |
| 31 | * |
| 32 | * @var array<int, string> |
| 33 | */ |
| 34 | protected $fillable = [ |
| 35 | 'user_id', |
| 36 | 'token', |
| 37 | 'expires_at', |
| 38 | ]; |
| 39 | |
| 40 | /** |
| 41 | * Преобразование атрибутов. |
| 42 | * |
| 43 | * @var array<string, string> |
| 44 | */ |
| 45 | protected $casts = [ |
| 46 | 'expires_at' => 'datetime', |
| 47 | ]; |
| 48 | |
| 49 | /** |
| 50 | * Get the user that owns the refresh token. |
| 51 | */ |
| 52 | public function user(): BelongsTo |
| 53 | { |
| 54 | return $this->belongsTo(User::class); |
| 55 | } |
| 56 | |
| 57 | /** |
| 58 | * Check if the refresh token is expired. |
| 59 | */ |
| 60 | public function isExpired(): bool |
| 61 | { |
| 62 | return $this->expires_at->isPast(); |
| 63 | } |
| 64 | |
| 65 | /** |
| 66 | * Scope to get only valid (non-expired) tokens. |
| 67 | */ |
| 68 | public function scopeValid($query) |
| 69 | { |
| 70 | return $query->where('expires_at', '>', now()); |
| 71 | } |
| 72 | } |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 3 |
+
+
+
+ |
+ 0.00% |
+ 0 / 3 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| Role | +
+
+
+ |
+ 0.00% |
+ 0 / 3 |
+
+
+
+ |
+ 0.00% |
+ 0 / 3 |
+ 12 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| users | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| permissions | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| hasPermissionTo | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Models; |
| 4 | |
| 5 | use Illuminate\Database\Eloquent\Factories\HasFactory; |
| 6 | |
| 7 | /** |
| 8 | * |
| 9 | * @class Role |
| 10 | * |
| 11 | * @property int $id |
| 12 | * @property string $name |
| 13 | * @property string|null $description |
| 14 | * @property \Illuminate\Support\Carbon|null $created_at |
| 15 | * @property \Illuminate\Support\Carbon|null $updated_at |
| 16 | */ |
| 17 | class Role extends BaseModel |
| 18 | { |
| 19 | use HasFactory; |
| 20 | |
| 21 | protected $fillable = ['name', 'description']; |
| 22 | |
| 23 | /** |
| 24 | * Роль может принадлежать многим пользователям. |
| 25 | */ |
| 26 | public function users() |
| 27 | { |
| 28 | return $this->belongsToMany(User::class); |
| 29 | } |
| 30 | |
| 31 | /** |
| 32 | * Роль может иметь много разрешений. |
| 33 | */ |
| 34 | public function permissions() |
| 35 | { |
| 36 | return $this->belongsToMany(Permission::class); |
| 37 | } |
| 38 | |
| 39 | /** |
| 40 | * Проверяет, имеет ли роль данное разрешение. |
| 41 | */ |
| 42 | public function hasPermissionTo($permissionName) |
| 43 | { |
| 44 | return $this->permissions->contains('name', $permissionName); |
| 45 | } |
| 46 | |
| 47 | } |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| Task | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| user | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Models; |
| 4 | |
| 5 | use Illuminate\Database\Eloquent\Factories\HasFactory; |
| 6 | |
| 7 | /** |
| 8 | * |
| 9 | * @class Task |
| 10 | * |
| 11 | * @property int $id |
| 12 | * @property int $user_id |
| 13 | * @property string $name |
| 14 | * @property string|null $description |
| 15 | * @property string $status |
| 16 | * @property string $priority |
| 17 | * @property \Illuminate\Support\Carbon|null $due_date |
| 18 | * @property \Illuminate\Support\Carbon|null $created_at |
| 19 | * @property \Illuminate\Support\Carbon|null $updated_at |
| 20 | */ |
| 21 | class Task extends BaseModel |
| 22 | { |
| 23 | use HasFactory; |
| 24 | |
| 25 | protected $fillable = [ |
| 26 | 'user_id', |
| 27 | 'name', |
| 28 | 'description', |
| 29 | 'status', |
| 30 | 'priority', |
| 31 | 'due_date', |
| 32 | ]; |
| 33 | |
| 34 | protected $casts = [ |
| 35 | 'due_date' => 'datetime', |
| 36 | ]; |
| 37 | |
| 38 | /** |
| 39 | * Задача принадлежит одному пользователю. |
| 40 | */ |
| 41 | public function user() |
| 42 | { |
| 43 | return $this->belongsTo(User::class); |
| 44 | } |
| 45 | } |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 63.16% |
+ 12 / 19 |
+
+
+
+ |
+ 50.00% |
+ 5 / 10 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| User | +
+
+
+ |
+ 63.16% |
+ 12 / 19 |
+
+
+
+ |
+ 50.00% |
+ 5 / 10 |
+ 15.00 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| casts | +
+
+
+ |
+ 100.00% |
+ 4 / 4 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 1 | ++ | ||
| roles | +
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 1 | ++ | ||
| tasks | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| hasPermissionTo | +
+
+
+ |
+ 0.00% |
+ 0 / 3 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| hasRole | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| isAdmin | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| notifications_settings | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| getJWTIdentifier | +
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 1 | ++ | ||
| getJWTCustomClaims | +
+
+
+ |
+ 100.00% |
+ 5 / 5 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 1 | ++ | ||
| profile | +
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 1 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Models; |
| 4 | |
| 5 | // use Illuminate\Contracts\Auth\MustVerifyEmail; |
| 6 | use Illuminate\Contracts\Queue\Job; |
| 7 | use Illuminate\Database\Eloquent\Factories\HasFactory; |
| 8 | use Illuminate\Foundation\Auth\User as Authenticatable; |
| 9 | use Illuminate\Notifications\Notifiable; |
| 10 | use PHPOpenSourceSaver\JWTAuth\Contracts\JWTSubject; |
| 11 | |
| 12 | /** |
| 13 | * |
| 14 | * @class User |
| 15 | * |
| 16 | * @property int $id |
| 17 | * @property string $name |
| 18 | * @property string $email |
| 19 | * @property \Illuminate\Support\Carbon|null $email_verified_at |
| 20 | * @property string $password |
| 21 | * @property string|null $remember_token |
| 22 | * @property \Illuminate\Support\Carbon|null $created_at |
| 23 | * @property \Illuminate\Support\Carbon|null $updated_at |
| 24 | */ |
| 25 | class User extends Authenticatable implements JWTSubject |
| 26 | { |
| 27 | /** @use HasFactory<\Database\Factories\UserFactory> */ |
| 28 | use HasFactory, Notifiable; |
| 29 | |
| 30 | /** |
| 31 | * The attributes that are mass assignable. |
| 32 | * |
| 33 | * @var list<string> |
| 34 | */ |
| 35 | protected $fillable = [ |
| 36 | 'name', |
| 37 | 'email', |
| 38 | 'password', |
| 39 | ]; |
| 40 | |
| 41 | /** |
| 42 | * The attributes that should be hidden for serialization. |
| 43 | * |
| 44 | * @var list<string> |
| 45 | */ |
| 46 | protected $hidden = [ |
| 47 | 'password', |
| 48 | 'remember_token', |
| 49 | ]; |
| 50 | |
| 51 | /** |
| 52 | * Get the attributes that should be cast. |
| 53 | * |
| 54 | * @return array<string, string> |
| 55 | */ |
| 56 | protected function casts(): array |
| 57 | { |
| 58 | return [ |
| 59 | 'email_verified_at' => 'datetime', |
| 60 | 'password' => 'hashed', |
| 61 | ]; |
| 62 | } |
| 63 | |
| 64 | /** |
| 65 | * Пользователь может иметь много ролей. |
| 66 | */ |
| 67 | public function roles() |
| 68 | { |
| 69 | return $this->belongsToMany(Role::class); |
| 70 | } |
| 71 | |
| 72 | /** |
| 73 | * Пользователь может иметь много задач. |
| 74 | */ |
| 75 | public function tasks() |
| 76 | { |
| 77 | return $this->hasMany(Task::class); |
| 78 | } |
| 79 | |
| 80 | /** |
| 81 | * Проверяет, имеет ли пользователь данное разрешение через любую из своих ролей. |
| 82 | */ |
| 83 | public function hasPermissionTo($permissionName) |
| 84 | { |
| 85 | return $this->roles->contains(function ($role) use ($permissionName) { |
| 86 | return $role->hasPermissionTo($permissionName); |
| 87 | }); |
| 88 | } |
| 89 | |
| 90 | /** |
| 91 | * Проверяет, принадлежит ли пользователь к данной роли. |
| 92 | */ |
| 93 | public function hasRole($roleName) |
| 94 | { |
| 95 | return $this->roles->contains('name', $roleName); |
| 96 | } |
| 97 | |
| 98 | public function isAdmin(): bool{ |
| 99 | return $this->hasRole('Administrator'); |
| 100 | } |
| 101 | |
| 102 | |
| 103 | public function notifications_settings() |
| 104 | { |
| 105 | return $this->hasOne(NotificationSettings::class); |
| 106 | } |
| 107 | |
| 108 | public function getJWTIdentifier(): int |
| 109 | { |
| 110 | return $this->id; |
| 111 | } |
| 112 | |
| 113 | public function getJWTCustomClaims(): array |
| 114 | { |
| 115 | return [ |
| 116 | 'roles' => $this->roles->pluck('id')->toArray(), |
| 117 | 'email' => $this->email, |
| 118 | 'name' => $this->name, |
| 119 | ]; |
| 120 | } |
| 121 | |
| 122 | public function profile() |
| 123 | { |
| 124 | return $this->hasOne(UserProfile::class); |
| 125 | } |
| 126 | } |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| UserProfile | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| user | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Models; |
| 4 | |
| 5 | use Illuminate\Database\Eloquent\Factories\HasFactory; |
| 6 | use Illuminate\Database\Eloquent\Model; |
| 7 | |
| 8 | /** |
| 9 | * App\Models\UserProfile |
| 10 | * |
| 11 | * @property int $id |
| 12 | * @property int $user_id |
| 13 | * @property string|null $phone |
| 14 | * @property string|null $address |
| 15 | * @property \Illuminate\Support\Carbon|null $birth_date |
| 16 | * @property string|null $about |
| 17 | * @property \Illuminate\Support\Carbon|null $created_at |
| 18 | * @property \Illuminate\Support\Carbon|null $updated_at |
| 19 | * @property-read \App\Models\User $user |
| 20 | * |
| 21 | * @method static \Database\Factories\UserProfileFactory factory(...$parameters) |
| 22 | * @method static \Illuminate\Database\Eloquent\Builder|UserProfile newModelQuery() |
| 23 | * @method static \Illuminate\Database\Eloquent\Builder|UserProfile newQuery() |
| 24 | * @method static \Illuminate\Database\Eloquent\Builder|UserProfile query() |
| 25 | * @method static \Illuminate\Database\Eloquent\Builder|UserProfile whereAbout($value) |
| 26 | * @method static \Illuminate\Database\Eloquent\Builder|UserProfile whereAddress($value) |
| 27 | * @method static \Illuminate\Database\Eloquent\Builder|UserProfile whereBirthDate($value) |
| 28 | * @method static \Illuminate\Database\Eloquent\Builder|UserProfile whereCreatedAt($value) |
| 29 | * @method static \Illuminate\Database\Eloquent\Builder|UserProfile whereId($value) |
| 30 | * @method static \Illuminate\Database\Eloquent\Builder|UserProfile wherePhone($value) |
| 31 | * @method static \Illuminate\Database\Eloquent\Builder|UserProfile whereUpdatedAt($value) |
| 32 | * @method static \Illuminate\Database\Eloquent\Builder|UserProfile whereUserId($value) |
| 33 | * |
| 34 | * @mixin \Eloquent |
| 35 | */ |
| 36 | class UserProfile extends Model |
| 37 | { |
| 38 | use HasFactory; |
| 39 | |
| 40 | protected $fillable = [ |
| 41 | 'user_id', |
| 42 | 'phone', |
| 43 | 'address', |
| 44 | 'birth_date', |
| 45 | 'about', |
| 46 | ]; |
| 47 | |
| 48 | public function user() |
| 49 | { |
| 50 | return $this->belongsTo(User::class); |
| 51 | } |
| 52 | } |
| Class | +Coverage | +
|---|---|
| App\Models\NotificationSettings | 0% |
| App\Models\Permission | 0% |
| App\Models\Role | 0% |
| App\Models\Task | 0% |
| App\Models\UserProfile | 0% |
| App\Models\RefreshToken | 33% |
| App\Models\User | 63% |
| Class | +CRAP | +
|---|
| Method | +Coverage | +
|---|---|
| user | 0% |
| roles | 0% |
| isExpired | 0% |
| scopeValid | 0% |
| users | 0% |
| permissions | 0% |
| hasPermissionTo | 0% |
| user | 0% |
| tasks | 0% |
| hasPermissionTo | 0% |
| hasRole | 0% |
| isAdmin | 0% |
| notifications_settings | 0% |
| user | 0% |
| Method | +CRAP | +
|---|
| + | Code Coverage |
+ ||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ ||||||
| Total | +
+
+
+ |
+ 48.39% |
+ 15 / 31 |
+
+
+
+ |
+ 36.36% |
+ 8 / 22 |
+
+
+
+ |
+ 22.22% |
+ 2 / 9 |
+
| + | n/a |
+ 0 / 0 |
+ + | n/a |
+ 0 / 0 |
+ + | n/a |
+ 0 / 0 |
+ |
|
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ |
|
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
|
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
|
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ |
|
+
+
+ |
+ 33.33% |
+ 1 / 3 |
+
+
+
+ |
+ 33.33% |
+ 1 / 3 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
|
+
+
+ |
+ 0.00% |
+ 0 / 3 |
+
+
+
+ |
+ 0.00% |
+ 0 / 3 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
|
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
|
+
+
+ |
+ 63.16% |
+ 12 / 19 |
+
+
+
+ |
+ 50.00% |
+ 5 / 10 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
|
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 26 |
+
+
+
+ |
+ 0.00% |
+ 0 / 5 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| ProductPriceChangedNotification | +
+
+
+ |
+ 0.00% |
+ 0 / 26 |
+
+
+
+ |
+ 0.00% |
+ 0 / 5 |
+ 72 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| __construct | +
+
+
+ |
+ 0.00% |
+ 0 / 5 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| via | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| toMail | +
+
+
+ |
+ 0.00% |
+ 0 / 10 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 12 | ++ | ||
| toArray | +
+
+
+ |
+ 0.00% |
+ 0 / 9 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 6 | ++ | ||
| formatPrice | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Notifications; |
| 4 | |
| 5 | use App\Models\Product; |
| 6 | use Illuminate\Bus\Queueable; |
| 7 | use Illuminate\Contracts\Queue\ShouldQueue; |
| 8 | use Illuminate\Notifications\Messages\MailMessage; |
| 9 | use Illuminate\Notifications\Notification; |
| 10 | |
| 11 | class ProductPriceChangedNotification extends Notification implements ShouldQueue |
| 12 | { |
| 13 | use Queueable; |
| 14 | |
| 15 | public int $productId; |
| 16 | public float $oldPrice; |
| 17 | public float $newPrice; |
| 18 | public string $productTitle; |
| 19 | public string $alias; |
| 20 | |
| 21 | /** |
| 22 | * Create a new notification instance. |
| 23 | */ |
| 24 | public function __construct(int $productId, float $oldPrice, float $newPrice, string $title, string $alias) |
| 25 | { |
| 26 | $this->productId = $productId; |
| 27 | $this->oldPrice = $oldPrice; |
| 28 | $this->newPrice = $newPrice; |
| 29 | $this->productTitle = $title; |
| 30 | $this->alias = $alias; |
| 31 | } |
| 32 | |
| 33 | /** |
| 34 | * Get the notification's delivery channels. |
| 35 | * |
| 36 | * @return array<int, string> |
| 37 | */ |
| 38 | public function via(object $notifiable): array |
| 39 | { |
| 40 | return ['mail', 'database']; |
| 41 | } |
| 42 | |
| 43 | /** |
| 44 | * Get the mail representation of the notification. |
| 45 | */ |
| 46 | public function toMail(object $notifiable): MailMessage |
| 47 | { |
| 48 | $priceChange = $this->newPrice > $this->oldPrice ? 'увеличилась' : 'снизилась'; |
| 49 | $priceChangeIcon = $this->newPrice > $this->oldPrice ? '📈' : '📉'; |
| 50 | |
| 51 | return (new MailMessage) |
| 52 | ->subject("Изменение цены на товар: {$this->productTitle}") |
| 53 | ->greeting("Здравствуйте, {$notifiable->name}!") |
| 54 | ->line("Цена на товар \"{$this->productTitle}\" {$priceChange} {$priceChangeIcon}") |
| 55 | ->line("Старая цена: {$this->formatPrice($this->oldPrice)}") |
| 56 | ->line("Новая цена: {$this->formatPrice($this->newPrice)}") |
| 57 | ->action('Посмотреть товар', url("/products/{$this->alias}")) |
| 58 | ->line('Спасибо за использование нашего сервиса!'); |
| 59 | } |
| 60 | |
| 61 | /** |
| 62 | * Get the array representation of the notification. |
| 63 | * |
| 64 | * @return array<string, mixed> |
| 65 | */ |
| 66 | public function toArray(object $notifiable): array |
| 67 | { |
| 68 | return [ |
| 69 | 'product_id' => $this->productId, |
| 70 | 'product_title' => $this->productTitle, |
| 71 | 'product_alias' => $this->alias, |
| 72 | 'old_price' => $this->oldPrice, |
| 73 | 'new_price' => $this->newPrice, |
| 74 | 'price_change_type' => $this->newPrice > $this->oldPrice ? 'increase' : 'decrease', |
| 75 | 'price_difference' => abs($this->newPrice - $this->oldPrice), |
| 76 | ]; |
| 77 | } |
| 78 | |
| 79 | /** |
| 80 | * Format price for display |
| 81 | */ |
| 82 | private function formatPrice(float $price): string |
| 83 | { |
| 84 | return number_format($price, 2, ',', ' ') . ' ₽'; |
| 85 | } |
| 86 | } |
| Class | +Coverage | +
|---|---|
| App\Notifications\ProductPriceChangedNotification | 0% |
| Class | +CRAP | +
|---|---|
| App\Notifications\ProductPriceChangedNotification | 72 |
| Method | +Coverage | +
|---|---|
| __construct | 0% |
| via | 0% |
| toMail | 0% |
| toArray | 0% |
| formatPrice | 0% |
| + | Code Coverage |
+ ||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ ||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 26 |
+
+
+
+ |
+ 0.00% |
+ 0 / 5 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
|
+
+
+ |
+ 0.00% |
+ 0 / 26 |
+
+
+
+ |
+ 0.00% |
+ 0 / 5 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 7 |
+
+
+
+ |
+ 0.00% |
+ 0 / 7 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| CategoryPolicy | +
+
+
+ |
+ 0.00% |
+ 0 / 7 |
+
+
+
+ |
+ 0.00% |
+ 0 / 7 |
+ 90 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| viewAny | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| view | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| create | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| update | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 6 | ++ | ||
| delete | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 6 | ++ | ||
| restore | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| forceDelete | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Policies; |
| 4 | |
| 5 | use App\Models\Category; |
| 6 | use App\Models\User; |
| 7 | use Illuminate\Auth\Access\Response; |
| 8 | |
| 9 | class CategoryPolicy |
| 10 | { |
| 11 | /** |
| 12 | * Determine whether the user can view any models. |
| 13 | */ |
| 14 | public function viewAny(User $user): bool |
| 15 | { |
| 16 | return true; |
| 17 | } |
| 18 | |
| 19 | /** |
| 20 | * Determine whether the user can view the model. |
| 21 | */ |
| 22 | public function view(User $user, Category $category): bool |
| 23 | { |
| 24 | return true; |
| 25 | } |
| 26 | |
| 27 | /** |
| 28 | * Determine whether the user can create models. |
| 29 | */ |
| 30 | public function create(User $user): bool |
| 31 | { |
| 32 | return $user->hasPermissionTo('create-category'); |
| 33 | } |
| 34 | |
| 35 | /** |
| 36 | * Determine whether the user can update the model. |
| 37 | */ |
| 38 | public function update(User $user, Category $category): bool |
| 39 | { |
| 40 | return $user->hasPermissionTo('edit-category') && $category->user_id === $user->id; |
| 41 | } |
| 42 | |
| 43 | /** |
| 44 | * Determine whether the user can delete the model. |
| 45 | */ |
| 46 | public function delete(User $user, Category $category): bool |
| 47 | { |
| 48 | return $user->hasPermissionTo('delete-category') && $category->user_id === $user->id; |
| 49 | } |
| 50 | |
| 51 | /** |
| 52 | * Determine whether the user can restore the model. |
| 53 | */ |
| 54 | public function restore(User $user, Category $category): bool |
| 55 | { |
| 56 | return false; |
| 57 | } |
| 58 | |
| 59 | /** |
| 60 | * Determine whether the user can permanently delete the model. |
| 61 | */ |
| 62 | public function forceDelete(User $user, Category $category): bool |
| 63 | { |
| 64 | return false; |
| 65 | } |
| 66 | } |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 7 |
+
+
+
+ |
+ 0.00% |
+ 0 / 7 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| ProductPolicy | +
+
+
+ |
+ 0.00% |
+ 0 / 7 |
+
+
+
+ |
+ 0.00% |
+ 0 / 7 |
+ 90 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| viewAny | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| view | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| create | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| update | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 6 | ++ | ||
| delete | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 6 | ++ | ||
| restore | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| forceDelete | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Policies; |
| 4 | |
| 5 | use App\Models\Product; |
| 6 | use App\Models\User; |
| 7 | use Illuminate\Auth\Access\Response; |
| 8 | |
| 9 | class ProductPolicy |
| 10 | { |
| 11 | /** |
| 12 | * Determine whether the user can view any models. |
| 13 | */ |
| 14 | public function viewAny(User $user): bool |
| 15 | { |
| 16 | return true; |
| 17 | } |
| 18 | |
| 19 | /** |
| 20 | * Determine whether the user can view the model. |
| 21 | */ |
| 22 | public function view(User $user, Product $product): bool |
| 23 | { |
| 24 | return true; |
| 25 | } |
| 26 | |
| 27 | /** |
| 28 | * Determine whether the user can create models. |
| 29 | */ |
| 30 | public function create(User $user): bool |
| 31 | { |
| 32 | return $user->hasPermissionTo('create-product'); |
| 33 | } |
| 34 | |
| 35 | /** |
| 36 | * Determine whether the user can update the model. |
| 37 | */ |
| 38 | public function update(User $user, Product $product): bool |
| 39 | { |
| 40 | return $user->hasPermissionTo('edit-product') && $product->user_id === $user->id; |
| 41 | } |
| 42 | |
| 43 | /** |
| 44 | * Determine whether the user can delete the model. |
| 45 | */ |
| 46 | public function delete(User $user, Product $product): bool |
| 47 | { |
| 48 | return $user->hasPermissionTo('delete-product') && $product->user_id === $user->id; |
| 49 | } |
| 50 | |
| 51 | /** |
| 52 | * Determine whether the user can restore the model. |
| 53 | */ |
| 54 | public function restore(User $user, Product $product): bool |
| 55 | { |
| 56 | return false; |
| 57 | } |
| 58 | |
| 59 | /** |
| 60 | * Determine whether the user can permanently delete the model. |
| 61 | */ |
| 62 | public function forceDelete(User $user, Product $product): bool |
| 63 | { |
| 64 | return false; |
| 65 | } |
| 66 | } |
| Class | +Coverage | +
|---|---|
| App\Policies\CategoryPolicy | 0% |
| App\Policies\ProductPolicy | 0% |
| Class | +CRAP | +
|---|---|
| App\Policies\CategoryPolicy | 90 |
| App\Policies\ProductPolicy | 90 |
| + | Code Coverage |
+ ||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ ||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 14 |
+
+
+
+ |
+ 0.00% |
+ 0 / 14 |
+
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+
|
+
+
+ |
+ 0.00% |
+ 0 / 7 |
+
+
+
+ |
+ 0.00% |
+ 0 / 7 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
|
+
+
+ |
+ 0.00% |
+ 0 / 7 |
+
+
+
+ |
+ 0.00% |
+ 0 / 7 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 55.56% |
+ 5 / 9 |
+
+
+
+ |
+ 50.00% |
+ 1 / 2 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| AppServiceProvider | +
+
+
+ |
+ 55.56% |
+ 5 / 9 |
+
+
+
+ |
+ 50.00% |
+ 1 / 2 |
+ 3.79 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| register | +
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 1 | ++ | ||
| boot | +
+
+
+ |
+ 50.00% |
+ 4 / 8 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2.50 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Providers; |
| 4 | |
| 5 | use App\Models\User; |
| 6 | use Illuminate\Support\Facades\Auth; |
| 7 | use Illuminate\Support\Facades\Gate; |
| 8 | use Illuminate\Support\ServiceProvider; |
| 9 | use App\Models\Category; |
| 10 | use App\Models\Product; |
| 11 | use App\Policies\CategoryPolicy; |
| 12 | use App\Policies\ProductPolicy; |
| 13 | |
| 14 | class AppServiceProvider extends ServiceProvider |
| 15 | { |
| 16 | /** |
| 17 | * Register any application services. |
| 18 | */ |
| 19 | public function register(): void |
| 20 | { |
| 21 | |
| 22 | } |
| 23 | |
| 24 | /** |
| 25 | * Bootstrap any application services. |
| 26 | */ |
| 27 | public function boot(): void |
| 28 | { |
| 29 | Gate::policy(Category::class, CategoryPolicy::class); |
| 30 | Gate::policy(Product::class, ProductPolicy::class); |
| 31 | |
| 32 | Auth::viaRequest('custom-token', function ($request) { |
| 33 | |
| 34 | $token = $request->bearerToken(); |
| 35 | |
| 36 | if($token === null){ |
| 37 | return null; |
| 38 | } |
| 39 | |
| 40 | return User::where('api_token', $token)->first(); |
| 41 | |
| 42 | |
| 43 | }); |
| 44 | } |
| 45 | } |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 5 |
+
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| EventServiceProvider | +
+
+
+ |
+ 0.00% |
+ 0 / 5 |
+
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+ 6 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| boot | +
+
+
+ |
+ 0.00% |
+ 0 / 4 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| shouldDiscoverEvents | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Providers; |
| 4 | |
| 5 | use App\Events\ProductPriceChanged; |
| 6 | use App\Listeners\HandleProductPriceChange; |
| 7 | use Illuminate\Auth\Events\Registered; |
| 8 | use Illuminate\Auth\Listeners\SendEmailVerificationNotification; |
| 9 | use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; |
| 10 | use Illuminate\Support\Facades\Event; |
| 11 | |
| 12 | class EventServiceProvider extends ServiceProvider |
| 13 | { |
| 14 | /** |
| 15 | * The event to listener mappings for the application. |
| 16 | * |
| 17 | * @var array<class-string, array<int, class-string>> |
| 18 | */ |
| 19 | protected $listen = [ |
| 20 | Registered::class => [ |
| 21 | SendEmailVerificationNotification::class, |
| 22 | ], |
| 23 | ]; |
| 24 | |
| 25 | /** |
| 26 | * Register any events for your application. |
| 27 | */ |
| 28 | public function boot(): void |
| 29 | { |
| 30 | Event::listen( |
| 31 | ProductPriceChanged::class, |
| 32 | HandleProductPriceChange::class |
| 33 | ); |
| 34 | } |
| 35 | |
| 36 | /** |
| 37 | * Determine if events and listeners should be automatically discovered. |
| 38 | */ |
| 39 | public function shouldDiscoverEvents(): bool |
| 40 | { |
| 41 | return false; |
| 42 | } |
| 43 | } |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 81.25% |
+ 13 / 16 |
+
+
+
+ |
+ 66.67% |
+ 2 / 3 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| RepositoryServiceProvider | +
+
+
+ |
+ 81.25% |
+ 13 / 16 |
+
+
+
+ |
+ 66.67% |
+ 2 / 3 |
+ 4.11 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| register | +
+
+
+ |
+ 100.00% |
+ 2 / 2 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 1 | ++ | ||
| boot | +
+
+
+ |
+ 100.00% |
+ 9 / 9 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 1 | ++ | ||
| warmCache | +
+
+
+ |
+ 40.00% |
+ 2 / 5 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2.86 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Providers; |
| 4 | |
| 5 | use App\Models\Product; |
| 6 | use Illuminate\Support\ServiceProvider; |
| 7 | use App\Repositories\Contracts\ProductRepositoryInterface; |
| 8 | use App\Repositories\Eloquent\ProductRepository; |
| 9 | use App\Repositories\Contracts\CategoryRepositoryInterface; |
| 10 | use App\Repositories\Eloquent\CategoryRepository; |
| 11 | |
| 12 | class RepositoryServiceProvider extends ServiceProvider |
| 13 | { |
| 14 | /** |
| 15 | * Register services. |
| 16 | */ |
| 17 | public function register(): void |
| 18 | { |
| 19 | $this->app->bind(ProductRepositoryInterface::class, ProductRepository::class); |
| 20 | $this->app->bind(CategoryRepositoryInterface::class, CategoryRepository::class); |
| 21 | } |
| 22 | |
| 23 | /** |
| 24 | * Bootstrap services. |
| 25 | */ |
| 26 | public function boot(): void |
| 27 | { |
| 28 | Product::created(function ($product) { |
| 29 | $this->warmCache(); |
| 30 | }); |
| 31 | |
| 32 | Product::updated(function ($product) { |
| 33 | $this->warmCache(); |
| 34 | }); |
| 35 | |
| 36 | Product::deleted(function ($product) { |
| 37 | $this->warmCache(); |
| 38 | }); |
| 39 | } |
| 40 | |
| 41 | protected function warmCache(): void |
| 42 | { |
| 43 | if (app()->runningInConsole()) { |
| 44 | return; |
| 45 | } |
| 46 | |
| 47 | dispatch(function () { |
| 48 | \Artisan::call('cache:warm-products'); |
| 49 | })->afterResponse(); |
| 50 | } |
| 51 | } |
| Class | +Coverage | +
|---|---|
| App\Providers\EventServiceProvider | 0% |
| App\Providers\AppServiceProvider | 55% |
| App\Providers\RepositoryServiceProvider | 81% |
| Class | +CRAP | +
|---|---|
| App\Providers\RepositoryServiceProvider | 4 |
| App\Providers\AppServiceProvider | 3 |
| + | Code Coverage |
+ ||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ ||||||
| Total | +
+
+
+ |
+ 60.00% |
+ 18 / 30 |
+
+
+
+ |
+ 42.86% |
+ 3 / 7 |
+
+
+
+ |
+ 0.00% |
+ 0 / 3 |
+
|
+
+
+ |
+ 55.56% |
+ 5 / 9 |
+
+
+
+ |
+ 50.00% |
+ 1 / 2 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
|
+
+
+ |
+ 0.00% |
+ 0 / 5 |
+
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
|
+
+
+ |
+ 81.25% |
+ 13 / 16 |
+
+
+
+ |
+ 66.67% |
+ 2 / 3 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | ++ | n/a |
+ 0 / 0 |
+ + | n/a |
+ 0 / 0 |
+ CRAP | ++ | n/a |
+ 0 / 0 |
+
| 1 | <?php |
| 2 | |
| 3 | namespace App\Repositories\Contracts; |
| 4 | |
| 5 | use App\Models\Category; |
| 6 | use Illuminate\Pagination\LengthAwarePaginator; |
| 7 | use Illuminate\Support\Collection; |
| 8 | |
| 9 | interface CategoryRepositoryInterface |
| 10 | { |
| 11 | public function getAllPaginated(int $perPage = 10, string $orderBy = 'order'): LengthAwarePaginator; |
| 12 | public function getAll(): Collection; |
| 13 | public function find(int $id): ?Category; |
| 14 | public function create(array $data): Category; |
| 15 | public function update(Category $category, array $data): Category; |
| 16 | public function delete(Category $category): bool; |
| 17 | |
| 18 | } |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | ++ | n/a |
+ 0 / 0 |
+ + | n/a |
+ 0 / 0 |
+ CRAP | ++ | n/a |
+ 0 / 0 |
+
| 1 | <?php |
| 2 | |
| 3 | namespace App\Repositories\Contracts; |
| 4 | |
| 5 | use App\Models\Product; |
| 6 | use Illuminate\Pagination\LengthAwarePaginator; |
| 7 | use Illuminate\Support\Collection; |
| 8 | |
| 9 | interface ProductRepositoryInterface |
| 10 | { |
| 11 | public function getAllPaginated(int $perPage = 10, string $orderBy = 'order'): LengthAwarePaginator; |
| 12 | public function find(int $id): ?Product; |
| 13 | public function create(array $data): Product; |
| 14 | public function update(Product $product, array $data): Product; |
| 15 | public function delete(Product $product): bool; |
| 16 | public function syncCategories(Product $product, array $categoryIds): void; |
| 17 | public function search(string $query, int $perPage = 10): LengthAwarePaginator; |
| 18 | public function count(): int; |
| 19 | } |
| Class | +Coverage | +
|---|
| Class | +CRAP | +
|---|
| Method | +Coverage | +
|---|
| Method | +CRAP | +
|---|
| + | Code Coverage |
+ ||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ ||||||
| Total | ++ | n/a |
+ 0 / 0 |
+ + | n/a |
+ 0 / 0 |
+ + | n/a |
+ 0 / 0 |
+
| + | n/a |
+ 0 / 0 |
+ + | n/a |
+ 0 / 0 |
+ + | n/a |
+ 0 / 0 |
+ |
| + | n/a |
+ 0 / 0 |
+ + | n/a |
+ 0 / 0 |
+ + | n/a |
+ 0 / 0 |
+ |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 8 |
+
+
+
+ |
+ 0.00% |
+ 0 / 7 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| CategoryRepository | +
+
+
+ |
+ 0.00% |
+ 0 / 8 |
+
+
+
+ |
+ 0.00% |
+ 0 / 7 |
+ 56 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| __construct | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| getAllPaginated | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| getAll | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| find | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| create | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| update | +
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| delete | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Repositories\Eloquent; |
| 4 | |
| 5 | use App\Models\Category; |
| 6 | use App\Repositories\Contracts\CategoryRepositoryInterface; |
| 7 | use Illuminate\Pagination\LengthAwarePaginator; |
| 8 | use Illuminate\Support\Collection; |
| 9 | |
| 10 | class CategoryRepository implements CategoryRepositoryInterface |
| 11 | { |
| 12 | protected Category $model; |
| 13 | |
| 14 | public function __construct(Category $model) |
| 15 | { |
| 16 | $this->model = $model; |
| 17 | } |
| 18 | |
| 19 | public function getAllPaginated(int $perPage = 10, string $orderBy = 'order'): LengthAwarePaginator |
| 20 | { |
| 21 | return $this->model->orderBy($orderBy)->paginate($perPage); |
| 22 | } |
| 23 | |
| 24 | public function getAll(): Collection |
| 25 | { |
| 26 | return $this->model->orderBy('title')->get(); |
| 27 | } |
| 28 | |
| 29 | public function find(int $id): ?Category |
| 30 | { |
| 31 | return $this->model->find($id); |
| 32 | } |
| 33 | |
| 34 | public function create(array $data): Category |
| 35 | { |
| 36 | return $this->model->create($data); |
| 37 | } |
| 38 | |
| 39 | public function update(Category $category, array $data): Category |
| 40 | { |
| 41 | $category->update($data); |
| 42 | return $category; |
| 43 | } |
| 44 | |
| 45 | public function delete(Category $category): bool |
| 46 | { |
| 47 | return $category->delete(); |
| 48 | } |
| 49 | } |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 4.00% |
+ 1 / 25 |
+
+
+
+ |
+ 10.00% |
+ 1 / 10 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| ProductRepository | +
+
+
+ |
+ 4.00% |
+ 1 / 25 |
+
+
+
+ |
+ 10.00% |
+ 1 / 10 |
+ 162.52 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| __construct | +
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+
+
+
+ |
+ 100.00% |
+ 1 / 1 |
+ 1 | ++ | ||
| count | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| getAllPaginated | +
+
+
+ |
+ 0.00% |
+ 0 / 4 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 6 | ++ | ||
| find | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| create | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| update | +
+
+
+ |
+ 0.00% |
+ 0 / 4 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| handlePriceChange | +
+
+
+ |
+ 0.00% |
+ 0 / 8 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 12 | ++ | ||
| delete | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| syncCategories | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| search | +
+
+
+ |
+ 0.00% |
+ 0 / 3 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Repositories\Eloquent; |
| 4 | |
| 5 | use App\DTO\ProductPriceData; |
| 6 | use App\Events\ProductPriceChanged; |
| 7 | use App\Models\Product; |
| 8 | use App\Repositories\Contracts\ProductRepositoryInterface; |
| 9 | use Illuminate\Pagination\LengthAwarePaginator; |
| 10 | use Illuminate\Support\Collection; |
| 11 | |
| 12 | class ProductRepository implements ProductRepositoryInterface |
| 13 | { |
| 14 | protected Product $model; |
| 15 | |
| 16 | public function __construct(Product $model) |
| 17 | { |
| 18 | $this->model = $model; |
| 19 | } |
| 20 | |
| 21 | public function count(): int |
| 22 | { |
| 23 | return $this->model->count(); |
| 24 | } |
| 25 | |
| 26 | public function getAllPaginated(int $perPage = 10, string $orderBy = 'order', int $page = null): LengthAwarePaginator |
| 27 | { |
| 28 | $query = $this->model->orderBy($orderBy); |
| 29 | |
| 30 | if ($page) { |
| 31 | return $query->paginate($perPage, ['*'], 'page', $page); |
| 32 | } |
| 33 | |
| 34 | return $query->paginate($perPage); |
| 35 | } |
| 36 | |
| 37 | public function find(int $id): ?Product |
| 38 | { |
| 39 | return $this->model->find($id); |
| 40 | } |
| 41 | |
| 42 | public function create(array $data): Product |
| 43 | { |
| 44 | return $this->model->create($data); |
| 45 | } |
| 46 | |
| 47 | public function update(Product $product, array $data): Product |
| 48 | { |
| 49 | $oldPrice = $product->price; |
| 50 | $product->update($data); |
| 51 | |
| 52 | $this->handlePriceChange($product, $oldPrice, $data); |
| 53 | |
| 54 | return $product; |
| 55 | } |
| 56 | |
| 57 | |
| 58 | protected function handlePriceChange(Product $product, float $oldPrice, array $data): void |
| 59 | { |
| 60 | if (isset($data['price']) && $product->price != $oldPrice) { |
| 61 | event(new ProductPriceChanged( |
| 62 | new ProductPriceData( |
| 63 | productId: $product->id, |
| 64 | oldPrice: $oldPrice, |
| 65 | newPrice: $data['price'] |
| 66 | ) |
| 67 | )); |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | public function delete(Product $product): bool |
| 72 | { |
| 73 | return $product->delete(); |
| 74 | } |
| 75 | |
| 76 | public function syncCategories(Product $product, array $categoryIds): void |
| 77 | { |
| 78 | $product->categories()->sync($categoryIds); |
| 79 | } |
| 80 | |
| 81 | public function search(string $query, int $perPage = 10): LengthAwarePaginator |
| 82 | { |
| 83 | return $this->model->where('title', 'like', '%' . $query . '%') |
| 84 | ->orWhere('text', 'like', '%' . $query . '%') |
| 85 | ->paginate($perPage); |
| 86 | } |
| 87 | } |
| Class | +Coverage | +
|---|---|
| App\Repositories\Eloquent\CategoryRepository | 0% |
| App\Repositories\Eloquent\ProductRepository | 4% |
| Class | +CRAP | +
|---|---|
| App\Repositories\Eloquent\ProductRepository | 162 |
| Method | +Coverage | +
|---|---|
| __construct | 0% |
| getAllPaginated | 0% |
| getAll | 0% |
| find | 0% |
| create | 0% |
| update | 0% |
| delete | 0% |
| count | 0% |
| getAllPaginated | 0% |
| find | 0% |
| create | 0% |
| update | 0% |
| handlePriceChange | 0% |
| delete | 0% |
| syncCategories | 0% |
| search | 0% |
| Method | +CRAP | +
|---|---|
| handlePriceChange | 12 |
| getAllPaginated | 6 |
| + | Code Coverage |
+ ||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ ||||||
| Total | +
+
+
+ |
+ 3.03% |
+ 1 / 33 |
+
+
+
+ |
+ 5.88% |
+ 1 / 17 |
+
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+
|
+
+
+ |
+ 0.00% |
+ 0 / 8 |
+
+
+
+ |
+ 0.00% |
+ 0 / 7 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
|
+
+
+ |
+ 4.00% |
+ 1 / 25 |
+
+
+
+ |
+ 10.00% |
+ 1 / 10 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
| Class | +Coverage | +
|---|---|
| App\Repositories\Eloquent\CategoryRepository | 0% |
| App\Repositories\Eloquent\ProductRepository | 4% |
| Class | +CRAP | +
|---|---|
| App\Repositories\Eloquent\ProductRepository | 162 |
| Method | +Coverage | +
|---|---|
| __construct | 0% |
| getAllPaginated | 0% |
| getAll | 0% |
| find | 0% |
| create | 0% |
| update | 0% |
| delete | 0% |
| count | 0% |
| getAllPaginated | 0% |
| find | 0% |
| create | 0% |
| update | 0% |
| handlePriceChange | 0% |
| delete | 0% |
| syncCategories | 0% |
| search | 0% |
| Method | +CRAP | +
|---|---|
| handlePriceChange | 12 |
| getAllPaginated | 6 |
| + | Code Coverage |
+ ||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ ||||||
| Total | +
+
+
+ |
+ 3.03% |
+ 1 / 33 |
+
+
+
+ |
+ 5.88% |
+ 1 / 17 |
+
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+
| + | n/a |
+ 0 / 0 |
+ + | n/a |
+ 0 / 0 |
+ + | n/a |
+ 0 / 0 |
+ |
|
+
+
+ |
+ 3.03% |
+ 1 / 33 |
+
+
+
+ |
+ 5.88% |
+ 1 / 17 |
+
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+ |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| AppLayout | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| render | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\View\Components; |
| 4 | |
| 5 | use Illuminate\View\Component; |
| 6 | use Illuminate\View\View; |
| 7 | |
| 8 | class AppLayout extends Component |
| 9 | { |
| 10 | /** |
| 11 | * Get the view / contents that represents the component. |
| 12 | */ |
| 13 | public function render(): View |
| 14 | { |
| 15 | return view('layouts.app'); |
| 16 | } |
| 17 | } |
| + | Code Coverage |
+ |||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ |||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ CRAP | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| GuestLayout | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
| render | +
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ 2 | ++ | ||
| 1 | <?php |
| 2 | |
| 3 | namespace App\View\Components; |
| 4 | |
| 5 | use Illuminate\View\Component; |
| 6 | use Illuminate\View\View; |
| 7 | |
| 8 | class GuestLayout extends Component |
| 9 | { |
| 10 | /** |
| 11 | * Get the view / contents that represents the component. |
| 12 | */ |
| 13 | public function render(): View |
| 14 | { |
| 15 | return view('layouts.guest'); |
| 16 | } |
| 17 | } |
| Class | +Coverage | +
|---|---|
| App\View\Components\AppLayout | 0% |
| App\View\Components\GuestLayout | 0% |
| Class | +CRAP | +
|---|
| + | Code Coverage |
+ ||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ ||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+
|
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
|
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+
+
+
+ |
+ 0.00% |
+ 0 / 1 |
+ |
| Class | +Coverage | +
|---|---|
| App\View\Components\AppLayout | 0% |
| App\View\Components\GuestLayout | 0% |
| Class | +CRAP | +
|---|
| + | Code Coverage |
+ ||||||||
| + | Lines |
+ Functions and Methods |
+ Classes and Traits |
+ ||||||
| Total | +
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+
|
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+
+
+
+ |
+ 0.00% |
+ 0 / 2 |
+ |
0)for(u=-1;++u","
"],col:[2,"
"],tr:[2,"","
"],td:[3,"
"],_default:[0,"",""]};function Se(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&fe(e,t)?ce.merge([e],n):n}function Ee(e,t){for(var n=0,r=e.length;n"," ").append(ce.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},ce.expr.pseudos.animated=function(t){return ce.grep(ce.timers,function(e){return t===e.elem}).length},ce.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=ce.css(e,"position"),c=ce(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=ce.css(e,"top"),u=ce.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),v(t)&&(t=t.call(e,n,ce.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},ce.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){ce.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===ce.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===ce.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=ce(e).offset()).top+=ce.css(e,"borderTopWidth",!0),i.left+=ce.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-ce.css(r,"marginTop",!0),left:t.left-i.left-ce.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===ce.css(e,"position"))e=e.offsetParent;return e||J})}}),ce.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;ce.fn[t]=function(e){return M(this,function(e,t,n){var r;if(y(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),ce.each(["top","left"],function(e,n){ce.cssHooks[n]=Ye(le.pixelPosition,function(e,t){if(t)return t=Ge(e,n),_e.test(t)?ce(e).position()[n]+"px":t})}),ce.each({Height:"height",Width:"width"},function(a,s){ce.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){ce.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return M(this,function(e,t,n){var r;return y(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?ce.css(e,t,i):ce.style(e,t,n,i)},s,n?e:void 0,n)}})}),ce.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){ce.fn[t]=function(e){return this.on(t,e)}}),ce.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.on("mouseenter",e).on("mouseleave",t||e)}}),ce.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){ce.fn[n]=function(e,t){return 0
"}),b},a.models.candlestickBarChart=function(){var b=a.models.historicalBarChart(a.models.candlestickBar());return b.useInteractiveGuideline(!0),b.interactiveLayer.tooltip.contentGenerator(function(a){var c=a.series[0].data,d=c.openopen: "+b.yAxis.tickFormat()(c.open)+" close: "+b.yAxis.tickFormat()(c.close)+" high "+b.yAxis.tickFormat()(c.high)+" low: "+b.yAxis.tickFormat()(c.low)+"
"}),b},a.models.legend=function(){"use strict";function b(p){function q(a,b){return"furious"!=o?"#000":m?a.disengaged?"#000":"#fff":m?void 0:(a.color||(a.color=g(a,b)),a.disabled?a.color:"#fff")}function r(a,b){return m&&"furious"==o&&a.disengaged?"#eee":a.color||g(a,b)}function s(a){return m&&"furious"==o?1:a.disabled?0:1}return p.each(function(b){var g=d-c.left-c.right,p=d3.select(this);a.utils.initSVG(p);var t=p.selectAll("g.nv-legend").data([b]),u=t.enter().append("g").attr("class","nvd3 nv-legend").append("g"),v=t.select("g");t.attr("transform","translate("+c.left+","+c.top+")");var w,x,y=v.selectAll(".nv-series").data(function(a){return"furious"!=o?a:a.filter(function(a){return m?!0:!a.disengaged})}),z=y.enter().append("g").attr("class","nv-series");switch(o){case"furious":x=23;break;case"classic":x=20}if("classic"==o)z.append("circle").style("stroke-width",2).attr("class","nv-legend-symbol").attr("r",5),w=y.select("circle");else if("furious"==o){z.append("rect").style("stroke-width",2).attr("class","nv-legend-symbol").attr("rx",3).attr("ry",3),w=y.select(".nv-legend-symbol"),z.append("g").attr("class","nv-check-box").property("innerHTML",'open: "+b.yAxis.tickFormat()(c.open)+" close: "+b.yAxis.tickFormat()(c.close)+" high "+b.yAxis.tickFormat()(c.high)+" low: "+b.yAxis.tickFormat()(c.low)+" Classes
+ Coverage Distribution
+ Complexity
+ Insufficient Coverage
+ Project Risks
+ Methods
+ Coverage Distribution
+ Complexity
+ Insufficient Coverage
+ Project Risks
+
+
+
+
+
+
+
+ Method
+ CRAP
+
+ update 56
+ handle 42
+ destroy 42
+ store 20
+ handle 12
+ __invoke 12
+ handle 12
+ toMail 12
+ handlePriceChange 12
+ store 6
+ store 6
+ __invoke 6
+ store 6
+ store 6
+ update 6
+ handle 6
+ authenticate 6
+ ensureIsNotRateLimited 6
+ __invoke 6
+ toArray 6
+ update 6
+ delete 6
+ update 6
+ delete 6
+ getAllPaginated 6
+ update 5
+ index 4
+ show 2
+ warmCache 2
+
+
+ boot 2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Total
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Console
+
+
+
+
+
+
+
+
+
+
+
+
+ DTO
+
+
+
+
+
+
+
+
+
+
+
+
+ Events
+
+
+
+
+
+
+
+
+
+
+
+
+ Http
+
+
+
+
+
+
+
+
+
+
+
+
+ Jobs
+
+
+
+
+
+
+
+
+
+
+
+
+ Listeners
+
+
+
+
+
+
+
+
+
+
+
+
+ Logging
+
+
+
+
+
+
+
+
+
+
+
+
+ Models
+
+
+
+
+
+
+
+
+
+
+
+
+ Notifications
+
+
+
+
+
+
+
+
+
+
+
+
+ Policies
+
+
+
+
+
+
+
+
+
+
+
+
+ Providers
+
+
+
+
+
+
+
+
+
+
+
+
+ Repositories
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ View
+
+
+
+
+
+
+
+
+