feature: product creation and image upload

create image upload endpoint
create product creation endpoint
create get product categories endpoint
This commit is contained in:
kusowl 2026-02-26 19:03:41 +05:30
parent 30bc4a0cf3
commit 684b7585bb
19 changed files with 431 additions and 0 deletions

View 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,
]);
}
}

View 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();
}
}

View 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,
]);
}
}

View 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,
);
}
}

View 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,
];
}
}

View 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']),
);
}
}

View 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();
}
}

View 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) {}
}

View 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();
}
}

View 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;
}
}

View 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',
];
}
}

View 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');
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ProductCategory extends Model
{
//
}

View 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);
}
}

View File

@ -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');
}
};

View File

@ -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');
}
};

View File

@ -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');
}
};

View 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']);
}
}

View File

@ -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']);