Merge branch 'backend'

This commit is contained in:
kusowl 2026-02-27 18:22:36 +05:30
commit 068975d3b0
29 changed files with 622 additions and 1 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,32 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\GeneratorCommand;
class MakeActionCommand extends GeneratorCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'make:action {name}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new action class';
protected function getStub(): string
{
return base_path('stubs/action.stub');
}
protected function getDefaultNamespace($rootNamespace): string
{
return $rootNamespace.'\Actions';
}
}

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,53 @@
<?php
namespace App\Data;
use App\Contracts\OutputDataTransferObject;
use App\Models\Product;
use App\Models\ProductImage;
final readonly class ProductDTO implements OutputDataTransferObject
{
/**
* @param ProductImageDTO[] $productImages
*/
public function __construct(
public int $id,
public string $title,
public string $description,
public int $actualPrice,
public int $listPrice,
public ProductCategoryDTO $category,
public array $productImages,
) {}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'id' => $this->id,
'title' => $this->title,
'description' => $this->description,
'actualPrice' => $this->actualPrice,
'listPrice' => $this->listPrice,
'category' => $this->category->toArray(),
'productImage' => array_map(fn (ProductImageDTO $productImage) => $productImage->toArray(),
$this->productImages),
];
}
public static function fromModel(Product $product): self
{
return new self(
id: $product->id,
title: $product->title,
description: $product->description,
actualPrice: $product->actual_price,
listPrice: $product->list_price,
category: ProductCategoryDTO::fromModel($product->category),
productImages: $product->images->map(fn (ProductImage $productImage) => ProductImageDTO::fromModel($productImage))->all(),
);
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Data;
use App\Contracts\OutputDataTransferObject;
use App\Models\ProductImage;
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,
];
}
public static function fromModel(ProductImage $productImage): self
{
return new self(
$productImage->id,
$productImage->path,
$productImage->product_id
);
}
}

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

@ -12,6 +12,7 @@ public function __construct(
public string $email, public string $email,
public string $mobileNumber, public string $mobileNumber,
public string $city, public string $city,
public string $role
) {} ) {}
/** /**
@ -25,6 +26,7 @@ public function toArray(): array
'email' => $this->email, 'email' => $this->email,
'mobileNumber' => $this->mobileNumber, 'mobileNumber' => $this->mobileNumber,
'city' => $this->city, 'city' => $this->city,
'role' => $this->role,
]; ];
} }
} }

View File

@ -0,0 +1,10 @@
<?php
namespace App\Enums;
enum UserRoles: string
{
case Admin = 'admin';
case Customer = 'customer';
case Broker = 'broker';
}

View File

@ -40,7 +40,8 @@ public function show()
name: $user->name, name: $user->name,
email: $user->email, email: $user->email,
mobileNumber: $user->mobile_number, mobileNumber: $user->mobile_number,
city: $user->city city: $user->city,
role: $user->role->value
); );
return response()->json($userDto->toArray()); return response()->json($userDto->toArray());

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,31 @@
<?php
namespace App\Http\Controllers;
use App\Data\ProductDTO;
use App\Http\Requests\CreateProductRequest;
use App\Http\Resources\ProductResource;
use App\Models\Product;
use Illuminate\Http\Request;
class ProductController extends Controller
{
public function index()
{
$paginator = Product::query()->with(['category:id,name,slug', 'images:id,path,product_id'])->paginate();
$paginatedDtos = $paginator->through(fn ($product) => ProductDTO::fromModel($product));
return ProductResource::collection($paginatedDtos);
}
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,16 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class ProductCollection extends ResourceCollection
{
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
];
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Http\Resources;
use App\Data\ProductDTO;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/**
* @property ProductDTO $resource
*/
class ProductResource extends JsonResource
{
public function toArray(Request $request): array
{
return $this->resource->toArray();
}
}

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 category(): BelongsTo
{
return $this->belongsTo(ProductCategory::class, 'product_category_id');
}
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

@ -3,6 +3,7 @@
namespace App\Models; namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail; // use Illuminate\Contracts\Auth\MustVerifyEmail;
use App\Enums\UserRoles;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
@ -23,6 +24,7 @@ class User extends Authenticatable
'password', 'password',
'city', 'city',
'mobile_number', 'mobile_number',
'role',
]; ];
/** /**
@ -45,6 +47,7 @@ protected function casts(): array
return [ return [
'email_verified_at' => 'datetime', 'email_verified_at' => 'datetime',
'password' => 'hashed', 'password' => 'hashed',
'role' => UserRoles::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,23 @@
<?php
use App\Enums\UserRoles;
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::table('users', function (Blueprint $table) {
$table->enum('role', array_column(UserRoles::cases(), 'value'))->default(UserRoles::Customer->value);
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('role');
});
}
};

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::get('/categories', [ProductCategoryController::class, 'index']);
Route::apiResource('products', ProductController::class);

14
backend/stubs/action.stub Normal file
View File

@ -0,0 +1,14 @@
<?php
namespace {{ namespace }};
final readonly class {{ class }}
{
/**
* Execute the action.
*/
public function execute()
{
// Your logic here
}
}