Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions app/Http/Controllers/ProductController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace App\Http\Controllers;

use App\Models\Product;

class ProductController extends Controller
{
public function index()
{
$products = Product::query()
->orderByDesc('id')
->get();

return response()->json([
'data' => $products,
]);
}

public function show(int $id)
{
$product = Product::query()->findOrFail($id);

return response()->json([
'data' => $product,
]);
}
}
18 changes: 18 additions & 0 deletions app/Models/Product.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
protected $fillable = [
'name',
'description',
'image_url',
];

protected $casts = [
'price' => 'integer',
];
}
26 changes: 26 additions & 0 deletions database/migrations/2026_01_28_134245_create_products_table.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

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('products', function (Blueprint $table) {
$table->id();
$table->string('name', 180);
$table->text('description')->nullable();
$table->unsignedInteger('price');
$table->string('image_url', 500)->nullable();
$table->timestamps();
});
}

public function down(): void
{
Schema::dropIfExists('products');
}
};
15 changes: 15 additions & 0 deletions database/seeders/DatabaseSeeder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
public function run(): void
{
$this->call([
ProductSeeder::class,
]);
}
}
49 changes: 49 additions & 0 deletions database/seeders/ProductSeeder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

namespace Database\Seeders;

use App\Models\Product;
use Illuminate\Database\Seeder;

class ProductSeeder extends Seeder
{
public function run(): void
{
$items = [
[
'name' => 'CloudShopt Hoodie',
'description' => 'Comfort hoodie with CloudShopt logo.',
'price' => 4990,
'image_url' => null,
],
[
'name' => 'CloudShopt T-Shirt',
'description' => 'Basic tee for everyday wear.',
'price' => 1990,
'image_url' => null,
],
[
'name' => 'Sticker Pack',
'description' => 'Set of 10 vinyl stickers.',
'price' => 590,
'image_url' => null,
],
[
'name' => 'Coffee Mug',
'description' => 'Ceramic mug 330ml.',
'price' => 1290,
'image_url' => null,
],
[
'name' => 'Notebook',
'description' => 'A5 dotted notebook.',
'price' => 990,
'image_url' => null,
],
];

foreach ($items as $item) {
Product::query()->create($item);
}
}
}
102 changes: 102 additions & 0 deletions docs/openapi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
openapi: 3.0.3
info:
title: CloudShopt Product Service API
version: 0.1.0
description: API for product catalog
servers:
- url: /api
tags:
- name: meta
- name: products

paths:
/info:
get:
tags: [meta]
summary: Service info (test endpoint)
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
service:
type: string
example: product-service
status:
type: string
example: ok

/health:
get:
tags: [meta]
summary: Health check
responses:
"200":
description: Healthy
content:
application/json:
schema:
type: object
properties:
ok:
type: boolean
example: true

/products:
get:
tags: [products]
summary: List products (test placeholder)
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/Product"
"500":
$ref: "#/components/responses/ServerError"

components:
responses:
ServerError:
description: Server error
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"

schemas:
Product:
type: object
properties:
id:
type: integer
example: 1
name:
type: string
example: "T-Shirt"
price:
type: number
format: float
example: 19.99

ErrorResponse:
type: object
properties:
message:
type: string
example: "Internal Server Error"
code:
type: string
example: "SERVER_ERROR"
details:
type: object
additionalProperties: true
22 changes: 22 additions & 0 deletions resources/views/swagger.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>API Docs</title>
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css" />
</head>
<body>
<div id="swagger-ui"></div>

<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
<script>
window.onload = () => {
SwaggerUIBundle({
url: "/api/products/openapi.yaml",
dom_id: "#swagger-ui",
});
};
</script>
</body>
</html>
89 changes: 18 additions & 71 deletions routes/api.php
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
<?php

use App\Http\Controllers\ProductController;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Route;

Route::get('/openapi.yaml', function () {
$path = base_path('docs/openapi.yaml');

abort_unless(file_exists($path), 404, 'openapi.yaml not found');

return response()->file($path, [
'Content-Type' => 'application/yaml; charset=utf-8',
'Cache-Control' => 'no-store',
]);
});

Route::get('/docs', function () {
return response()->view('swagger');
});

Route::get('/info', function () {
return response()->json([
'ok11' => true,
Expand Down Expand Up @@ -36,74 +52,5 @@
}
});





Route::get('/products', function () {
return response()->json([
[
'id' => 1,
'name' => 'CloudShopt Hoodie',
'price' => 49.90,
'currency' => 'EUR',
'sku' => 'CS-HOODIE-001',
'in_stock' => true,
],
[
'id' => 2,
'name' => 'CloudShopt T-Shirt',
'price' => 19.90,
'currency' => 'EUR',
'sku' => 'CS-TSHIRT-001',
'in_stock' => true,
],
[
'id' => 3,
'name' => 'CloudShopt Mug',
'price' => 12.90,
'currency' => 'EUR',
'sku' => 'CS-MUG-001',
'in_stock' => false,
],
]);
});


Route::get('/products/{id}', function (string $id) {
$products = [
1 => [
'id' => 1,
'name' => 'CloudShopt Hoodie',
'price' => 49.90,
'currency' => 'EUR',
'sku' => 'CS-HOODIE-001',
'in_stock' => true,
'description' => 'Warm hoodie for cloud-native builders.',
],
2 => [
'id' => 2,
'name' => 'CloudShopt T-Shirt',
'price' => 19.90,
'currency' => 'EUR',
'sku' => 'CS-TSHIRT-001',
'in_stock' => true,
'description' => 'Soft cotton tee with CloudShopt logo.',
],
3 => [
'id' => 3,
'name' => 'CloudShopt Mug',
'price' => 12.90,
'currency' => 'EUR',
'sku' => 'CS-MUG-001',
'in_stock' => false,
'description' => 'Ceramic mug for your morning deploys.',
],
];

if (!isset($products[(int)$id])) {
return response()->json(['message' => 'Product not found'], 404);
}

return response()->json($products[(int)$id]);
});
Route::get('/items', [ProductController::class, 'index']);
Route::get('/items/{id}', [ProductController::class, 'show']);