Add comprehensive project documentation for ClinicPro in CLAUDE.md and README.md

- Introduced CLAUDE.md for internal guidance on project structure, commands, and architecture.
- Created README.md with detailed project overview, technology stack, directory structure, setup instructions, API endpoints, authentication flow, and external services.
This commit is contained in:
hamed
2026-06-10 11:12:15 +03:30
parent c7ae591e49
commit e9075e8c92
18 changed files with 3533 additions and 142 deletions
+258
View File
@@ -8,12 +8,14 @@ use App\Blog\Repository\BlogRepository;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Controller\BaseController;
use App\Shared\Service\FileValidatorService;
use OpenApi\Attributes as OA;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\CurrentUser;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[OA\Tag(name: 'Blog')]
class BlogController extends BaseController
{
public function __construct(
@@ -24,6 +26,35 @@ class BlogController extends BaseController
// ── Public list/detail ────────────────────────────────────────────────────
#[OA\Get(
path: '/api/v1/blogs',
summary: 'List published blog posts (paginated)',
parameters: [
new OA\Parameter(name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1)),
new OA\Parameter(name: 'limit', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 20, maximum: 50)),
],
responses: [
new OA\Response(
response: 200,
description: 'Paginated list of published blog posts',
content: new OA\JsonContent(
properties: [
new OA\Property(property: 'success', type: 'boolean', example: true),
new OA\Property(property: 'data', type: 'array', items: new OA\Items(type: 'object')),
new OA\Property(
property: 'meta',
properties: [
new OA\Property(property: 'totalRecords', type: 'integer'),
new OA\Property(property: 'totalPages', type: 'integer'),
new OA\Property(property: 'currentPage', type: 'integer'),
],
type: 'object'
),
]
)
),
]
)]
#[Route('/api/v1/blogs', methods: ['GET'])]
public function list(Request $request): JsonResponse
{
@@ -36,6 +67,38 @@ class BlogController extends BaseController
return $this->paginated($blogs, $total, $page, $limit);
}
#[OA\Get(
path: '/api/v1/blog/{slug}',
summary: 'Get a published blog post by slug or UUID',
parameters: [
new OA\Parameter(
name: 'slug',
in: 'path',
required: true,
description: 'Blog slug or UUID',
schema: new OA\Schema(type: 'string')
),
],
responses: [
new OA\Response(
response: 200,
description: 'Blog post detail',
content: new OA\JsonContent(
properties: [
new OA\Property(property: 'success', type: 'boolean', example: true),
new OA\Property(
property: 'data',
properties: [
new OA\Property(property: 'data', type: 'object'),
],
type: 'object'
),
]
)
),
new OA\Response(response: 404, description: 'Blog post not found or not published'),
]
)]
#[Route('/api/v1/blog/{slug}', methods: ['GET'])]
public function detail(string $slug): JsonResponse
{
@@ -49,6 +112,63 @@ class BlogController extends BaseController
// ── Admin CRUD ────────────────────────────────────────────────────────────
#[OA\Post(
path: '/api/v1/blog',
summary: 'Create a new blog post (admin only)',
security: [['bearerAuth' => []]],
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(
required: ['title', 'body'],
properties: [
new OA\Property(property: 'title', type: 'string'),
new OA\Property(property: 'body', type: 'string'),
new OA\Property(property: 'summary', type: 'string', nullable: true),
new OA\Property(property: 'tags', type: 'array', items: new OA\Items(type: 'string'), nullable: true),
new OA\Property(property: 'status', type: 'string', enum: ['draft', 'published'], nullable: true),
]
)
),
responses: [
new OA\Response(
response: 201,
description: 'Blog post created',
content: new OA\JsonContent(
properties: [
new OA\Property(property: 'success', type: 'boolean', example: true),
new OA\Property(
property: 'data',
properties: [
new OA\Property(property: 'data', type: 'object'),
],
type: 'object'
),
]
)
),
new OA\Response(response: 401, description: 'Unauthorized'),
new OA\Response(response: 403, description: 'Forbidden — admin role required'),
new OA\Response(
response: 422,
description: 'Validation error',
content: new OA\JsonContent(
properties: [
new OA\Property(property: 'success', type: 'boolean', example: false),
new OA\Property(
property: 'errors',
type: 'array',
items: new OA\Items(
properties: [
new OA\Property(property: 'code', type: 'string'),
new OA\Property(property: 'message', type: 'string'),
]
)
),
]
)
),
]
)]
#[IsGranted('ROLE_ADMIN')]
#[Route('/api/v1/blog', methods: ['POST'])]
public function create(Request $request, #[CurrentUser] User $user): JsonResponse
@@ -76,6 +196,52 @@ class BlogController extends BaseController
return $this->success(['data' => $blog->toArray()], 201);
}
#[OA\Patch(
path: '/api/v1/blog/{uuid}',
summary: 'Update a blog post (admin only)',
security: [['bearerAuth' => []]],
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(
properties: [
new OA\Property(property: 'title', type: 'string', nullable: true),
new OA\Property(property: 'body', type: 'string', nullable: true),
new OA\Property(property: 'summary', type: 'string', nullable: true),
new OA\Property(property: 'tags', type: 'array', items: new OA\Items(type: 'string'), nullable: true),
new OA\Property(property: 'status', type: 'string', enum: ['draft', 'published'], nullable: true),
]
)
),
parameters: [
new OA\Parameter(
name: 'uuid',
in: 'path',
required: true,
schema: new OA\Schema(type: 'string', format: 'uuid')
),
],
responses: [
new OA\Response(
response: 200,
description: 'Blog post updated',
content: new OA\JsonContent(
properties: [
new OA\Property(property: 'success', type: 'boolean', example: true),
new OA\Property(
property: 'data',
properties: [
new OA\Property(property: 'data', type: 'object'),
],
type: 'object'
),
]
)
),
new OA\Response(response: 401, description: 'Unauthorized'),
new OA\Response(response: 403, description: 'Forbidden — admin role required'),
new OA\Response(response: 404, description: 'Blog post not found'),
]
)]
#[IsGranted('ROLE_ADMIN')]
#[Route('/api/v1/blog/{uuid}', methods: ['PATCH'])]
public function update(string $uuid, Request $request): JsonResponse
@@ -97,6 +263,40 @@ class BlogController extends BaseController
return $this->success(['data' => $blog->toArray()]);
}
#[OA\Delete(
path: '/api/v1/blog/{uuid}',
summary: 'Delete a blog post (admin only)',
security: [['bearerAuth' => []]],
parameters: [
new OA\Parameter(
name: 'uuid',
in: 'path',
required: true,
schema: new OA\Schema(type: 'string', format: 'uuid')
),
],
responses: [
new OA\Response(
response: 200,
description: 'Blog post deleted',
content: new OA\JsonContent(
properties: [
new OA\Property(property: 'success', type: 'boolean', example: true),
new OA\Property(
property: 'data',
properties: [
new OA\Property(property: 'message', type: 'string'),
],
type: 'object'
),
]
)
),
new OA\Response(response: 401, description: 'Unauthorized'),
new OA\Response(response: 403, description: 'Forbidden — admin role required'),
new OA\Response(response: 404, description: 'Blog post not found'),
]
)]
#[IsGranted('ROLE_ADMIN')]
#[Route('/api/v1/blog/{uuid}', methods: ['DELETE'])]
public function delete(string $uuid): JsonResponse
@@ -112,6 +312,64 @@ class BlogController extends BaseController
// ── Image upload ──────────────────────────────────────────────────────────
#[OA\Post(
path: '/file/upload/clinic_pro/blog/field_image',
summary: 'Upload a blog post image (admin only)',
security: [['bearerAuth' => []]],
requestBody: new OA\RequestBody(
required: true,
content: new OA\MediaType(
mediaType: 'multipart/form-data',
schema: new OA\Schema(
required: ['file'],
properties: [
new OA\Property(property: 'file', type: 'string', format: 'binary'),
new OA\Property(property: 'blog_uuid', type: 'string', format: 'uuid', nullable: true),
]
)
)
),
responses: [
new OA\Response(
response: 200,
description: 'Image uploaded',
content: new OA\JsonContent(
properties: [
new OA\Property(property: 'success', type: 'boolean', example: true),
new OA\Property(
property: 'data',
properties: [
new OA\Property(property: 'image_url', type: 'string'),
new OA\Property(property: 'filename', type: 'string'),
],
type: 'object'
),
]
)
),
new OA\Response(response: 401, description: 'Unauthorized'),
new OA\Response(response: 403, description: 'Forbidden — admin role required'),
new OA\Response(
response: 422,
description: 'No file provided or invalid file',
content: new OA\JsonContent(
properties: [
new OA\Property(property: 'success', type: 'boolean', example: false),
new OA\Property(
property: 'errors',
type: 'array',
items: new OA\Items(
properties: [
new OA\Property(property: 'code', type: 'string'),
new OA\Property(property: 'message', type: 'string'),
]
)
),
]
)
),
]
)]
#[IsGranted('ROLE_ADMIN')]
#[Route('/file/upload/clinic_pro/blog/field_image', methods: ['POST'])]
public function uploadImage(Request $request): JsonResponse