@@ -373,6 +373,9 @@ <h3 class="text-lg font-semibold mb-3 mt-6">Creating a Repository</h3>
373373 -> get();
374374 }
375375}</ code > </ pre >
376+ < p class ="text-gray-600 mb-4 ">
377+ The repository extends < code class ="bg-gray-100 px-2 py-1 rounded "> RecordRepository</ code > , which provides a < code class ="bg-gray-100 px-2 py-1 rounded "> create()</ code > method that accepts an array of data and returns the created model instance. This method handles model instantiation, property assignment, saving, and cache invalidation automatically.
378+ </ p >
376379 </ section >
377380
378381 <!-- Authentication -->
@@ -415,24 +418,91 @@ <h3 class="text-lg font-semibold mb-3 mt-6">Getting the Current User</h3>
415418
416419 < h3 class ="text-lg font-semibold mb-3 mt-6 "> Protecting Routes</ h3 >
417420 < p class ="text-gray-600 mb-4 ">
418- Use the AuthMiddleware to protect routes that require authentication:
421+ Use the AuthMiddleware to protect routes that require authentication. It's best practice to apply middleware at the class level when all methods require the same middleware :
419422 </ p >
420423 < pre class ="bg-gray-100 p-4 rounded mb-4 "> < code class ="language-php "> use App\Modules\ForgeAuth\Middlewares\AuthMiddleware;
421424use Forge\Core\Http\Attributes\Middleware;
422425
423- #[Route("/todos")]
426+ #[Service]
427+ #[Middleware("web")]
424428#[Middleware("App\Modules\ForgeAuth\Middlewares\AuthMiddleware")]
425- public function index(): Response
429+ final class TodoController
430+ {
431+ // All methods in this controller require authentication
432+ #[Route("/todos")]
433+ public function index(): Response
434+ {
435+ // This route requires authentication
436+ }
437+ }</ code > </ pre >
438+ < p class ="text-gray-600 mb-4 ">
439+ This approach is cleaner than repeating the middleware attribute on each method, and ensures all routes in the controller are protected.
440+ </ p >
441+ </ section >
442+
443+ <!-- Data Transfer Objects -->
444+ < section id ="data-transfer-objects " class ="section-anchor mb-12 ">
445+ < h2 class ="text-2xl font-bold text-gray-900 mb-4 "> Data Transfer Objects (DTOs)</ h2 >
446+ < p class ="text-gray-600 mb-6 ">
447+ Before creating our controller, let's create a DTO (Data Transfer Object) for creating todos. DTOs provide several benefits:
448+ </ p >
449+ < ul class ="list-disc list-inside space-y-2 text-gray-600 mb-6 ">
450+ < li > < strong > Type Safety:</ strong > DTOs enforce type checking and ensure data integrity</ li >
451+ < li > < strong > Validation:</ strong > Centralized validation logic for input data</ li >
452+ < li > < strong > Documentation:</ strong > Clear contract of what data is expected</ li >
453+ < li > < strong > Maintainability:</ strong > Changes to data structure are isolated to the DTO</ li >
454+ < li > < strong > Security:</ strong > Prevents mass assignment vulnerabilities by explicitly defining allowed fields</ li >
455+ </ ul >
456+
457+ < h3 class ="text-lg font-semibold mb-3 "> Creating CreateTodoDTO</ h3 >
458+ < p class ="text-gray-600 mb-4 ">
459+ Create < code class ="bg-gray-100 px-2 py-1 rounded "> app/Dto/CreateTodoDTO.php</ code > :
460+ </ p >
461+ < pre class ="bg-gray-100 p-4 rounded mb-4 "> < code class ="language-php "> <?php
462+
463+ declare(strict_types=1);
464+
465+ namespace App\Dto;
466+
467+ final class CreateTodoDTO
426468{
427- // This route requires authentication
469+ public function __construct(
470+ public string $title,
471+ public ?string $description = null,
472+ public bool $completed = false,
473+ ) {
474+ }
475+
476+ public static function fromArray(array $data): self
477+ {
478+ return new self(
479+ title: (string)($data['title'] ?? ''),
480+ description: isset($data['description']) && $data['description'] !== ''
481+ ? (string)$data['description']
482+ : null,
483+ completed: isset($data['completed']) && (bool)$data['completed'],
484+ );
485+ }
486+
487+ public function toArray(): array
488+ {
489+ return [
490+ 'title' => $this-> title,
491+ 'description' => $this-> description,
492+ 'completed' => $this-> completed,
493+ ];
494+ }
428495}</ code > </ pre >
496+ < p class ="text-gray-600 mb-4 ">
497+ This DTO defines the structure for creating a todo. The < code class ="bg-gray-100 px-2 py-1 rounded "> fromArray()</ code > method safely converts request data into a typed DTO instance, while < code class ="bg-gray-100 px-2 py-1 rounded "> toArray()</ code > converts it back to an array for database operations.
498+ </ p >
429499 </ section >
430500
431501 <!-- Controllers & Routes -->
432502 < section id ="controllers-routes " class ="section-anchor mb-12 ">
433503 < h2 class ="text-2xl font-bold text-gray-900 mb-4 "> Controllers & Routes</ h2 >
434504 < p class ="text-gray-600 mb-6 ">
435- Now let's create our TodoController with full CRUD operations.
505+ Now let's create our TodoController with full CRUD operations. We'll use descriptive method names (not Laravel-style) and follow best practices by using DTOs and the repository pattern.
436506 </ p >
437507
438508 < h3 class ="text-lg font-semibold mb-3 "> Creating TodoController</ h3 >
@@ -445,7 +515,7 @@ <h3 class="text-lg font-semibold mb-3">Creating TodoController</h3>
445515
446516namespace App\Controllers;
447517
448- use App\Models\Todo ;
518+ use App\Dto\CreateTodoDTO ;
449519use App\Modules\ForgeAuth\Middlewares\AuthMiddleware;
450520use App\Modules\ForgeAuth\Services\ForgeAuthService;
451521use App\Repositories\TodoRepository;
@@ -461,6 +531,7 @@ <h3 class="text-lg font-semibold mb-3">Creating TodoController</h3>
461531
462532#[Service]
463533#[Middleware("web")]
534+ #[Middleware("App\Modules\ForgeAuth\Middlewares\AuthMiddleware")]
464535final class TodoController
465536{
466537 use ControllerHelper;
@@ -472,7 +543,6 @@ <h3 class="text-lg font-semibold mb-3">Creating TodoController</h3>
472543 ) {}
473544
474545 #[Route("/todos")]
475- #[Middleware("App\Modules\ForgeAuth\Middlewares\AuthMiddleware")]
476546 public function index(): Response
477547 {
478548 $user = $this-> auth-> user();
@@ -485,8 +555,7 @@ <h3 class="text-lg font-semibold mb-3">Creating TodoController</h3>
485555 }
486556
487557 #[Route("/todos", "POST")]
488- #[Middleware("App\Modules\ForgeAuth\Middlewares\AuthMiddleware")]
489- public function store(Request $request): Response
558+ public function createTodo(Request $request): Response
490559 {
491560 $user = $this-> auth-> user();
492561 $data = $this-> sanitize($request-> postData);
@@ -496,23 +565,24 @@ <h3 class="text-lg font-semibold mb-3">Creating TodoController</h3>
496565 return Redirect::to("/todos");
497566 }
498567
499- $todo = new Todo();
500- $todo-> user_id = $user-> id;
501- $todo-> title = $data['title'];
502- $todo-> description = $data['description'] ?? null;
503- $todo-> completed = false;
504- $todo-> save();
568+ $dto = CreateTodoDTO::fromArray($data);
569+
570+ $todo = $this-> repository-> create([
571+ 'user_id' => $user-> id,
572+ 'title' => $dto-> title,
573+ 'description' => $dto-> description,
574+ 'completed' => $dto-> completed,
575+ ]);
505576
506577 Flash::set("success", "Todo created successfully");
507578 return Redirect::to("/todos");
508579 }
509580
510581 #[Route("/todos/{id}", "PATCH")]
511- #[Middleware("App\Modules\ForgeAuth\Middlewares\AuthMiddleware")]
512- public function update(Request $request, string $id): Response
582+ public function updateTodo(Request $request, string $id): Response
513583 {
514584 $user = $this-> auth-> user();
515- $todo = $this-> repository-> findById ((int)$id);
585+ $todo = $this-> repository-> find ((int)$id);
516586
517587 if ($todo === null || $todo-> user_id !== $user-> id) {
518588 Flash::set("error", "Todo not found");
@@ -521,30 +591,32 @@ <h3 class="text-lg font-semibold mb-3">Creating TodoController</h3>
521591
522592 $data = $this-> sanitize($request-> postData);
523593
594+ $updateData = [];
524595 if (isset($data['title'])) {
525- $todo- > title = $data['title'];
596+ $updateData[' title'] = $data['title'];
526597 }
527598
528599 if (isset($data['description'])) {
529- $todo- > description = $data['description'];
600+ $updateData[' description'] = $data['description'];
530601 }
531602
532603 if (isset($data['completed'])) {
533- $todo- > completed = (bool)$data['completed'];
604+ $updateData[' completed'] = (bool)$data['completed'];
534605 }
535606
536- $todo-> save();
607+ if (!empty($updateData)) {
608+ $this-> repository-> update($todo, $updateData);
609+ }
537610
538611 Flash::set("success", "Todo updated successfully");
539612 return Redirect::to("/todos");
540613 }
541614
542615 #[Route("/todos/{id}/toggle", "POST")]
543- #[Middleware("App\Modules\ForgeAuth\Middlewares\AuthMiddleware")]
544616 public function toggle(string $id): Response
545617 {
546618 $user = $this-> auth-> user();
547- $todo = $this-> repository-> findById ((int)$id);
619+ $todo = $this-> repository-> find ((int)$id);
548620
549621 if ($todo === null || $todo-> user_id !== $user-> id) {
550622 Flash::set("error", "Todo not found");
@@ -558,18 +630,17 @@ <h3 class="text-lg font-semibold mb-3">Creating TodoController</h3>
558630 }
559631
560632 #[Route("/todos/{id}", "DELETE")]
561- #[Middleware("App\Modules\ForgeAuth\Middlewares\AuthMiddleware")]
562- public function destroy(string $id): Response
633+ public function deleteTodo(string $id): Response
563634 {
564635 $user = $this-> auth-> user();
565- $todo = $this-> repository-> findById ((int)$id);
636+ $todo = $this-> repository-> find ((int)$id);
566637
567638 if ($todo === null || $todo-> user_id !== $user-> id) {
568639 Flash::set("error", "Todo not found");
569640 return Redirect::to("/todos");
570641 }
571642
572- $todo- > delete();
643+ $this- > repository- > delete($todo );
573644 Flash::set("success", "Todo deleted successfully");
574645
575646 return Redirect::to("/todos");
@@ -793,7 +864,7 @@ <h3 class="text-lg font-semibold mb-3">Creating the TodoList Wire Component</h3>
793864 #[Action]
794865 public function toggleTodo(int $todoId): void
795866 {
796- $todo = $this-> repository-> findById ($todoId);
867+ $todo = $this-> repository-> find ($todoId);
797868 if ($todo === null) {
798869 return;
799870 }
@@ -810,7 +881,7 @@ <h3 class="text-lg font-semibold mb-3">Creating the TodoList Wire Component</h3>
810881 #[Action]
811882 public function deleteTodo(int $todoId): void
812883 {
813- $todo = $this-> repository-> findById ($todoId);
884+ $todo = $this-> repository-> find ($todoId);
814885 if ($todo === null) {
815886 return;
816887 }
0 commit comments