Skip to content

Commit 01884d6

Browse files
committed
updated tutorial
1 parent eec97d5 commit 01884d6

1 file changed

Lines changed: 101 additions & 30 deletions

File tree

tutorial-building-todo-app.html

Lines changed: 101 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -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;
421424
use 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">&lt;?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

446516
namespace App\Controllers;
447517

448-
use App\Models\Todo;
518+
use App\Dto\CreateTodoDTO;
449519
use App\Modules\ForgeAuth\Middlewares\AuthMiddleware;
450520
use App\Modules\ForgeAuth\Services\ForgeAuthService;
451521
use 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")]
464535
final 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

Comments
 (0)