feature: product creation and image upload
create image upload endpoint create product creation endpoint create get product categories endpoint
This commit is contained in:
parent
30bc4a0cf3
commit
684b7585bb
16
backend/app/Actions/CreateProductAction.php
Normal file
16
backend/app/Actions/CreateProductAction.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Actions;
|
||||||
|
|
||||||
|
use App\Data\UploadImageDTO;
|
||||||
|
|
||||||
|
final readonly class CreateProductAction
|
||||||
|
{
|
||||||
|
public function execute(UploadImageDTO $uploadImageDTO): void
|
||||||
|
{
|
||||||
|
$path = $uploadImageDTO->image->store('public/images');
|
||||||
|
$uploadImageDTO->product->images()->create([
|
||||||
|
'path' => $path,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
backend/app/Actions/GetAllProductCategory.php
Normal file
19
backend/app/Actions/GetAllProductCategory.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Actions;
|
||||||
|
|
||||||
|
use App\Data\ProductCategoryDTO;
|
||||||
|
use App\Models\ProductCategory;
|
||||||
|
|
||||||
|
final readonly class GetAllProductCategory
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Execute the action.
|
||||||
|
*/
|
||||||
|
public function execute(): array
|
||||||
|
{
|
||||||
|
return ProductCategory::all(columns: ['id', 'name', 'slug'])
|
||||||
|
->map(fn ($category) => ProductCategoryDTO::fromModel($category))
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
16
backend/app/Actions/UploadImageAction.php
Normal file
16
backend/app/Actions/UploadImageAction.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Actions;
|
||||||
|
|
||||||
|
use App\Data\UploadImageDTO;
|
||||||
|
|
||||||
|
final readonly class UploadImageAction
|
||||||
|
{
|
||||||
|
public function execute(UploadImageDTO $uploadImageDTO): void
|
||||||
|
{
|
||||||
|
$path = $uploadImageDTO->image->store('public/images');
|
||||||
|
$uploadImageDTO->product->images()->create([
|
||||||
|
'path' => $path,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
36
backend/app/Data/ProductCategoryDTO.php
Normal file
36
backend/app/Data/ProductCategoryDTO.php
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Data;
|
||||||
|
|
||||||
|
use App\Contracts\OutputDataTransferObject;
|
||||||
|
use App\Models\ProductCategory;
|
||||||
|
|
||||||
|
final readonly class ProductCategoryDTO implements OutputDataTransferObject
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public int $id,
|
||||||
|
public string $name,
|
||||||
|
public string $slug,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'name' => $this->name,
|
||||||
|
'slug' => $this->slug,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function fromModel(ProductCategory $category): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
id: $category->id,
|
||||||
|
name: $category->name,
|
||||||
|
slug: $category->slug,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
backend/app/Data/ProductImageDTO.php
Normal file
26
backend/app/Data/ProductImageDTO.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Data;
|
||||||
|
|
||||||
|
use App\Contracts\OutputDataTransferObject;
|
||||||
|
|
||||||
|
final readonly class ProductImageDTO implements OutputDataTransferObject
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public ?int $id = null,
|
||||||
|
public ?string $path = null,
|
||||||
|
public ?int $productId = null,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'path' => $this->path,
|
||||||
|
'productId' => $this->productId,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
22
backend/app/Data/UploadImageDTO.php
Normal file
22
backend/app/Data/UploadImageDTO.php
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Data;
|
||||||
|
|
||||||
|
use App\Models\Product;
|
||||||
|
use Illuminate\Http\UploadedFile;
|
||||||
|
|
||||||
|
readonly class UploadImageDTO
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public UploadedFile $image,
|
||||||
|
public Product $product,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public static function fromRequest(array $data): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
image: $data['image'],
|
||||||
|
product: Product::find($data['product_id']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
backend/app/Http/Controllers/ProductCategoryController.php
Normal file
13
backend/app/Http/Controllers/ProductCategoryController.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Actions\GetAllProductCategory;
|
||||||
|
|
||||||
|
class ProductCategoryController extends Controller
|
||||||
|
{
|
||||||
|
public function index(GetAllProductCategory $action)
|
||||||
|
{
|
||||||
|
return $action->execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
23
backend/app/Http/Controllers/ProductController.php
Normal file
23
backend/app/Http/Controllers/ProductController.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Requests\CreateProductRequest;
|
||||||
|
use App\Models\Product;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class ProductController extends Controller
|
||||||
|
{
|
||||||
|
public function index() {}
|
||||||
|
|
||||||
|
public function store(CreateProductRequest $request)
|
||||||
|
{
|
||||||
|
return Product::create($request->validated());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(Product $product) {}
|
||||||
|
|
||||||
|
public function update(Request $request, Product $product) {}
|
||||||
|
|
||||||
|
public function destroy(Product $product) {}
|
||||||
|
}
|
||||||
30
backend/app/Http/Controllers/ProductImagesController.php
Normal file
30
backend/app/Http/Controllers/ProductImagesController.php
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Actions\UploadImageAction;
|
||||||
|
use App\Data\UploadImageDTO;
|
||||||
|
use App\Http\Requests\UploadImageRequest;
|
||||||
|
use App\Models\ProductImages;
|
||||||
|
|
||||||
|
class ProductImagesController extends Controller
|
||||||
|
{
|
||||||
|
public function store(UploadImageRequest $request, UploadImageAction $action)
|
||||||
|
{
|
||||||
|
$action->execute(UploadImageDTO::fromRequest($request->validated()));
|
||||||
|
|
||||||
|
return response()->json(['message' => 'Image uploaded successfully']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(ProductImages $productImages)
|
||||||
|
{
|
||||||
|
return $productImages;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(ProductImages $productImages)
|
||||||
|
{
|
||||||
|
$productImages->delete();
|
||||||
|
|
||||||
|
return response()->json();
|
||||||
|
}
|
||||||
|
}
|
||||||
24
backend/app/Http/Requests/CreateProductRequest.php
Normal file
24
backend/app/Http/Requests/CreateProductRequest.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class CreateProductRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'title' => 'required|string|max:255',
|
||||||
|
'description' => 'required|string',
|
||||||
|
'product_category_id' => 'required|exists:product_categories,id',
|
||||||
|
'actual_price' => 'required|numeric|min:0',
|
||||||
|
'list_price' => 'required|numeric|min:0',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
29
backend/app/Http/Requests/UploadImageRequest.php
Normal file
29
backend/app/Http/Requests/UploadImageRequest.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class UploadImageRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'image' => 'required|image|mimes:jpeg,png,jpg,gif,svg|max:2048',
|
||||||
|
'product_id' => 'required|exists:products,id',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
28
backend/app/Models/Product.php
Normal file
28
backend/app/Models/Product.php
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
class Product extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'title',
|
||||||
|
'actual_price',
|
||||||
|
'list_price',
|
||||||
|
'description',
|
||||||
|
'product_category_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function productCategory(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(ProductCategory::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function images(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(ProductImage::class, 'product_id', 'id');
|
||||||
|
}
|
||||||
|
}
|
||||||
10
backend/app/Models/ProductCategory.php
Normal file
10
backend/app/Models/ProductCategory.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class ProductCategory extends Model
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
19
backend/app/Models/ProductImage.php
Normal file
19
backend/app/Models/ProductImage.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class ProductImage extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'path',
|
||||||
|
'product_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function product(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Product::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('product_categories', function (Blueprint $table) {
|
||||||
|
$table->id()->index();
|
||||||
|
$table->string('name')->unique();
|
||||||
|
$table->string('slug')->unique();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('product_categories');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\ProductCategory;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('products', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('title');
|
||||||
|
$table->text('description');
|
||||||
|
$table->decimal('actual_price', 10, 2);
|
||||||
|
$table->decimal('list_price', 10, 2);
|
||||||
|
$table->foreignIdFor(ProductCategory::class)->index();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('products');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Product;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('product_images', function (Blueprint $table) {
|
||||||
|
$table->id()->index();
|
||||||
|
$table->string('path');
|
||||||
|
$table->foreignIdFor(Product::class)->index();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('product_images');
|
||||||
|
}
|
||||||
|
};
|
||||||
28
backend/database/seeders/ProductCategorySeeder.php
Normal file
28
backend/database/seeders/ProductCategorySeeder.php
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\ProductCategory;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class ProductCategorySeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$names = [
|
||||||
|
'Electronics', 'Clothing', 'Home', 'Books',
|
||||||
|
'Sports', 'Toys', 'Health', 'Beauty', 'Automotive',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Transform the flat names into an array of associative arrays
|
||||||
|
$categories = collect($names)->map(function ($name) {
|
||||||
|
return [
|
||||||
|
'name' => $name,
|
||||||
|
'slug' => Str::slug($name),
|
||||||
|
];
|
||||||
|
})->toArray();
|
||||||
|
|
||||||
|
ProductCategory::upsert($categories, ['name'], ['slug']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Http\Controllers\AuthenticatedUserController;
|
use App\Http\Controllers\AuthenticatedUserController;
|
||||||
|
use App\Http\Controllers\ProductCategoryController;
|
||||||
|
use App\Http\Controllers\ProductController;
|
||||||
|
use App\Http\Controllers\ProductImagesController;
|
||||||
use App\Http\Controllers\RegisteredUserController;
|
use App\Http\Controllers\RegisteredUserController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
@ -11,4 +14,7 @@
|
|||||||
Route::middleware('auth:sanctum')->group(function () {
|
Route::middleware('auth:sanctum')->group(function () {
|
||||||
Route::get('/user', [AuthenticatedUserController::class, 'show']);
|
Route::get('/user', [AuthenticatedUserController::class, 'show']);
|
||||||
Route::post('/logout', [AuthenticatedUserController::class, 'destroy']);
|
Route::post('/logout', [AuthenticatedUserController::class, 'destroy']);
|
||||||
|
Route::post('/upload/images', action: [ProductImagesController::class, 'store']);
|
||||||
|
Route::apiResource('products', ProductController::class);
|
||||||
});
|
});
|
||||||
|
Route::get('/categories', [ProductCategoryController::class, 'index']);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user