23.3 C
New York
Saturday, June 27, 2026

I Constructed a Tiny Journal App to Be taught Laravel. Here is the Course of, Step by Step.


Most Laravel tutorials hand you working code and stroll you thru it. That is nice for seeing how the items match, but it surely does not educate you a lot about what occurs while you depart a discipline clean, sort the unsuitable parameter identify, or hesitate earlier than working a command you do not totally belief but.

So as a substitute of following a tutorial, I constructed one thing small from scratch: Tiny Journal, a single-page journaling app. Write an entry, learn it again, edit it, delete it. No auth, no tags, no search. Sufficiently small to complete in a weekend, sufficiently big to hit actual Laravel ideas alongside the way in which.

That is the method I adopted, so as, together with the components that broke.

What You may Want

  • Primary PHP data (variables, capabilities, arrays)
  • An area Laravel set up
  • Familiarity with MVC at a conceptual stage, you do not want Laravel expertise particularly

Step 1: The Mannequin and Migration

The whole lot begins with one mannequin, Entry, and one migration:

php artisan make:mannequin Entry -m
Schema::create('entries', operate (Blueprint $desk) {
    $table->id();
    $table->string('title');
    $table->textual content('content material');
    $table->timestamps();
});
php artisan migrate

Two fields, title and content material, each required on the database stage. That final element issues greater than it seems prefer it ought to, it is the explanation the very first actual bug on this undertaking existed in any respect.

Step 2: Studying Entries (index and present)

With the desk in place, the primary working function was simply displaying entries:

public operate index()
{
    $entries = Entry::all();
    return view('entries.index', ['entries' => $entries]);
}

public operate present(Entry $entry)
{
    return view('entries.present', ['entry' => $entry]);
}

present() already makes use of route mannequin binding right here, Laravel matches the {entry} placeholder within the path to the Entry $entry parameter and fetches the document robotically, no handbook discover() wanted. I did not totally recognize this till later, after I tried writing edit() the good distance and seen the mismatch (extra on that under).

Step 3: Creating Entries (create and retailer)

public operate create()
{
    return view('entries.create');
}

public operate retailer(Request $request)
{
    $entry = new Entry();
    $entry->title = $request->title;
    $entry->content material = $request->content material;
    $entry->save();

    return redirect('/');
}

This labored, proper up till I submitted the shape with the title left clean. What got here again was a full-page database error, a NOT NULL constraint violation, straight from the migration in Step 1. The database was appropriately imposing a rule I would written myself, simply within the worst attainable place, after the shape had already submitted, with a stack hint as a substitute of a sentence the consumer might act on.

The repair is validation, known as earlier than something touches the database:

public operate retailer(Request $request)
{
    $request->validate([
        'title' => 'required',
        'content' => 'required',
    ]);

    $entry = new Entry();
    $entry->title = $request->title;
    $entry->content material = $request->content material;
    $entry->save();

    return redirect('/');
}

validate() both lets the request by or stops it and redirects again to the shape robotically. To really show the error, loop over Laravel’s error bag within the view:

@foreach ($errors->all() as $error)
    <p>{{ $error }}</p>
@endforeach

One gotcha: $errors is not a plain array you may loop over straight, it is a MessageBag, and also you want ->all() to drag the message strings out. Loop over $errors itself and also you get nothing, no error, no crash, simply silence.

Step 4: Enhancing Entries (edit and replace)

public operate edit($id)
{
    $entry = Entry::discover($id);
    return view('entries.edit', ['entry' => $entry]);
}

public operate replace(Request $request, Entry $entry)
{
    $entry->title = $request->enter('title');
    $entry->content material = $request->enter('content material');
    $entry->save();

    return redirect('/');
}

Two issues surfaced right here. First, the identical blank-title crash from Step 3 was nonetheless attainable on replace(), because it had no validation but. The repair is similar, simply sequenced appropriately, validate earlier than writing something to $entry, not after:

public operate replace(Request $request, Entry $entry)
{
    $request->validate([
        'title' => 'required',
        'content' => 'required',
    ]);

    $entry->title = $request->enter('title');
    $entry->content material = $request->enter('content material');
    $entry->save();

    return redirect('/');
}

Second, evaluating edit() and replace() facet by facet made route mannequin binding click on. replace() makes use of Entry $entry and by no means calls discover(). edit() nonetheless used the handbook $id + Entry::discover($id) sample. As soon as I rewrote edit() to match:

public operate edit(Entry $entry)
{
    return view('entries.edit', ['entry' => $entry]);
}

The rule grew to become apparent: route mannequin binding works each time the URL incorporates an ID pointing at one thing that already exists. That is true for edit, replace, and destroy. It is by no means true for retailer or index, there is not any particular document in these URLs to bind to within the first place.

Step 5: A Subtler Bug, Previous Enter

After including validation, one thing seemed proper for the unsuitable purpose. Sort an actual title, depart content material clean, submit. The web page reloads, and the title discipline nonetheless reveals what I typed. That seemed like outdated enter working. It wasn’t, the title was simply $entry->title, untouched, as a result of validation stopped the save earlier than something reached the database.

The precise repair is the outdated() helper:

<enter sort="textual content" identify="title" worth="{{ outdated(&#x27;title&#x27;, $entry->title) }}">
<textarea identify="content material">{{ outdated('content material', $entry->content material) }}</textarea>

outdated('title', $entry->title) checks for leftover enter from a failed submission first. If it exists, it wins. If the web page opened contemporary, it falls again to the entry’s saved worth. The true take a look at: change the title, clean out the content material, submit. If it is wired up appropriately, your edited title survives the spherical journey as a substitute of reverting.

Step 6: Deleting Entries, and Collapsing Six Routes Into One

public operate destroy(Entry $entry)
{
    $entry->delete();
    return redirect('/');
}

By this level I had six hand-written routes, one per motion:

Route::get('/entries', [EntryController::class, 'index']);
Route::get('/entries/create', [EntryController::class, 'create']);
Route::submit('/entries', [EntryController::class, 'store']);
Route::get('/entries/{entry}/edit', [EntryController::class, 'edit']);
Route::put('/entries/{entry}', [EntryController::class, 'update']);
Route::delete('/entries/{entry}', [EntryController::class, 'destroy']);

Laravel collapses all six into one line:

Route::useful resource('entries', EntryController::class);

I sat on this for longer than I ought to have, satisfied switching would power modifications elsewhere. It did not. The controller did not change in any respect, solely the routes file did. Route::useful resource() is not producing new habits, it is the identical six routes, similar names, similar verbs, pre-assembled.

Step 7: Cleansing Up With a Shared Structure

By the point every little thing labored, all 4 views (index, create, edit, present) had their very own full , , and “ tags, copy-pasted 4 instances. A shared structure fixes this:

{{-- sources/views/layouts/app.blade.php --}}



    <title>Tiny Journal</title>
    <hyperlink rel="stylesheet" href="{{ asset(&#x27;css/app.css&#x27;) }}">


    @yield('content material')


Every view shrinks to simply its distinctive content material:

{{-- sources/views/entries/index.blade.php --}}
@extends('layouts.app')

@part('content material')
    <h1>My Tiny Journal</h1>
    @foreach ($entries as $entry)
        <h2>{{ $entry->title }}</h2>
        <p>{{ $entry->content material }}</p>
        <a href="/entries/{{ $entry->id }}">View</a>
        <a href="/entries/{{ $entry->id }}/edit">Edit</a>
    @endforeach
@endsection

Add a stylesheet hyperlink to the structure as soon as, and each web page picks it up robotically, no different file wants touching.

The Partitions, Briefly

Wanting again on the course of above, the precise friction factors have been:

  1. A clean title crashing the app (Step 3), the database imposing a rule on the unsuitable layer.
  2. **The identical crash hiding in **replace() (Step 4), the identical repair, simply needing right sequencing.
  3. Route mannequin binding clicking (Step 4), as soon as I in contrast a technique utilizing it in opposition to one which wasn’t.
  4. Previous enter trying prefer it labored earlier than it really did (Step 5), two completely different mechanisms that produced identical-looking output.
  5. **Hesitating earlier than **Route::useful resource() (Step 6), concern of a change that turned out to be protected.
  6. 4 duplicated web page shells (Step 7), mounted as soon as the repetition received annoying sufficient to note.

None of those have been individually laborious. What was laborious was noticing, within the second, {that a} bug or a hesitation was pointing at a lacking idea, not a lacking line of code.

The place Tiny Journal Goes From Right here

Just a few apparent subsequent steps for those who’re constructing alongside:

  • Flash messages after create, replace, and delete, so the consumer will get affirmation as a substitute of a silent redirect
  • Stronger validation guidelines, minimal lengths, character limits, relatively than simply required
  • Mushy deletes, so “delete” does not imply completely gone

If you happen to’re studying Laravel proper now: construct one thing small, break it on function, and skim the error as a substitute of pasting it right into a search bar instantly. The framework normally already has a solution for no matter repetition or fragility you are feeling. You simply must hit the wall first to know which query to ask.



Supply hyperlink

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles