You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs: fix tutorials to use entities, Latte views, and marko up
- Replace manual SQL with entity-driven schemas (#[Table], #[Column])
- Replace inline HTML with Latte templates in resources/views/
- Use marko up instead of manual PHP server commands
- Note SSE multi-worker support in chat tutorial
- Reorder tutorials sidebar: Create a Custom Module first
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: docs/src/content/docs/tutorials/build-a-blog.md
+82-20Lines changed: 82 additions & 20 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -47,7 +47,58 @@ Run the migrations:
47
47
marko db:migrate
48
48
```
49
49
50
-
This creates the `posts`, `comments`, and related tables from the blog package's migrations.
50
+
The blog package defines its schema using entity attributes --- `#[Table]`, `#[Column]`, and `#[Index]` --- on entity classes like `Post` and `Comment`. When you run `marko db:migrate`, it reads these attributes and auto-generates the migrations. Here is a simplified view of the `Post` entity:
Templates access entity properties directly --- `$post->title`, `$post->slug`, `$post->summary` --- and use getter methods like `$post->getPublishedAt()` for computed values.
161
+
101
162
## Step 6: Add Authentication
102
163
103
164
Protect the comment form so only logged-in users can comment:
@@ -136,18 +197,19 @@ declare(strict_types=1);
136
197
137
198
namespace App\Blog\Plugin;
138
199
139
-
use Marko\Blog\Repositories\PostRepository;
200
+
use Marko\Blog\Entity\Post;
201
+
use Marko\Blog\Repositories\PostRepositoryInterface;
140
202
141
203
class AddReadingTimePlugin
142
204
{
143
-
public function afterFindBySlug(PostRepository $subject, ?array $result): ?array
205
+
public function afterFindBySlug(PostRepositoryInterface $subject, ?Post $result): ?Post
The prefix ensures all chat channels are namespaced under `chat:` in Redis, keeping them separate from other PubSub traffic in your application.
48
49
49
-
## Step 3: Create the Messages Table
50
+
## Step 3: Define the Message Entity
50
51
51
-
Create a migration for persisting chat messages:
52
+
Marko uses entity-driven schemas --- define your table structure as a PHP class with attributes, then run `marko db:migrate` to auto-generate and apply the migration.
The `#[Middleware(AuthMiddleware::class)]` attribute at the class level protects every endpoint in this controller. The `PublisherInterface` is injected by the DI container --- since `marko/pubsub-redis` is installed, it resolves to the `RedisPublisher` automatically.
216
+
The `#[Middleware(AuthMiddleware::class)]` attribute at the class level protects every endpoint in this controller. The `PublisherInterface` is injected by the DI container --- since `marko/pubsub-redis` is installed, it resolves to the `RedisPublisher` automatically. The `room` method renders a Latte template via `ViewInterface` --- the template name `'chat::message/room'` resolves to `resources/views/message/room.latte` within the chat module.
187
217
188
218
## Step 6: Build the SSE Streaming Endpoint
189
219
@@ -256,14 +286,16 @@ Key design decisions:
256
286
-**`timeout: 300`** --- The stream closes after 5 minutes. The client's `EventSource` will automatically reconnect, sending `Last-Event-ID` so no messages are lost.
257
287
-**Replay on reconnect** --- Before subscribing to the live stream, `replayMissed` sends any messages the client missed during the disconnection gap.
258
288
259
-
## Step 7: Add Client-Side JavaScript
289
+
## Step 7: Add the Chat View
290
+
291
+
Marko uses Latte templates stored in `resources/views/` within each module. The template name `'chat::message/room'` in the controller resolves to this file:
The template receives `$room` and `$messages` from the controller. Existing messages are rendered server-side in the `{foreach}` loop, while new messages arrive in real-time via the SSE connection below.
371
+
332
372
The `EventSource` API handles reconnection automatically. When the SSE stream closes (after the 300-second timeout or a network interruption), the browser reconnects and sends the last received event ID via the `Last-Event-ID` header. The server uses this to replay any missed messages before resuming the live stream.
333
373
334
374
Note that `source.addEventListener` uses the event name `room.general` --- this matches the `channel` field on the `Message`, which `SseStream` sets as the SSE `event` type via `SseEvent`.
@@ -404,12 +444,12 @@ The replay loop creates `SseEvent` objects with explicit `id` values. When the b
404
444
405
445
## Step 9: Start the Server and Test
406
446
407
-
Start the built-in PHP server:
408
-
409
447
```bash
410
-
php -S localhost:8000 -t public
448
+
marko up
411
449
```
412
450
451
+
`marko up` (alias for `dev:up`) starts the full development environment automatically --- PHP server, Docker if detected, pub/sub listener if pubsub packages are installed, and frontend build tools if detected. For SSE applications this is important: `marko up` starts PHP with `PHP_CLI_SERVER_WORKERS=4` by default, so the SSE connection does not block all other requests on the single-threaded PHP built-in server. MarkoTalk (the reference chat implementation) uses this same approach.
0 commit comments