First impression: Laravel
Tags: laravel, php
Published: 2021-10-28 12:01:17

It's hugely popular. Thousands of stars on GitHub, Podcasts dedicated to it, online courses are teaching it, and I'm yet to give it a shot. That changes today as I explore it and share: my first impressions of Laravel! 

>If you rather skip my journey on building my first Laravel-project you can go <a href="#impressions">straight to a summary of my impressions</a>.

## Where to start
There are many places you can go when you want to explore such a popular project as Laravel. There are Laracasts - the dedicated learning platform for everything about Laravel, there are huge swaths of tutorials on YouTube, and blog posts of course. I'll make it easy on my self and get my news straight from the horses mouth: https://laravel.com

### The PHP Framework for Web Artisans
🤔 What are Web Artisans? This is the first question popping up in my head as I visit the official homepage. I get the feeling it is developers that write **clean code**. So it would look like that this framework had this as a focus. *Clean* and *modular* perhaps, with many components ready to be imported into any project I might have. 


Reading up on some of the defining features we have: 
"dependency injection, an expressive database abstraction layer, queues and scheduled jobs, unit and integration testing, and more." 
It looks to me like I won't be terribly lost coming here from Symfony. 

I'm off to a good start, so I'll just jump on with the "Get started" documentation.

### Getting started
So an FYI, I'm coding on a laptop running Windows 10. I've got Docker Desktop and WSL 2 for the times I just want to throw some new database engine at a project. *Which is good to have* as we can parse from the installation walkthrough. 

The first step to install Laravel (with Docker) is to run the command `curl -s https://laravel.build/example-app | bash`. It took me some Googling to understand that this needs to be run from within WSL. This is a point-deduction for the docs. So here are the actual steps I took:
1. Make sure WSL2 is installed with `wsl -l -v`. I got the following result: ```bash
  NAME                   STATE           VERSION
* docker-desktop-data    Running         2
  docker-desktop         Running         2
```
2. Ubuntu is a good WSL dirtro to have. Install it and configure Docker to use it:
    a. `wsl --install -d Ubuntu`
    b. Open *Docker Desktop* and go into *Settings/Resources/WSL INTEGRATION* and enable *Ubuntu* as an additional distro.

3. Log on to the WSL Ubuntu distro: `wsl -d ubuntu` (You can use PowerShell, Command Prompt or Windows Terminal. I wasn't able to logon from Git Bash for some reason 🤷‍♂️)

4. From within WSL run `curl -s https://laravel.build/example-app | bash`. This command will now run and create the skeleton of a Larvel project in a new directory *example-app*.

5. Move into the newly created directory: `cd example-app`

6. Then build the docker images with the Laravel-tool *sail* and start the containers: `./vendor/bin/sail up`. At firsts this threw an error at me, so I rebooted my laptop and tried it again from within WSL. 

7. The Docker containers are all running. Just check out localhost and there's the proof.
![localhost shows a documents page for Laravel](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x9o3kxu429ofy2aoiwfi.png)
 

## Can I make a blog out of this?
So everything seems to be working. I've got the Laravel structure in place and Docker containers running - so what can I build with this? For a first project I'm thinking a blog post should be possible. But first I should see what I've got code-wise. Here's the directories I've got in the project.

```
app
bootstrap
config
database
public
resources
routes
storage
tests
vendor
```

For some reason, I love to check out the config alternatives first. Coming from *Symfony* I am thorougly surprised that YAML is nowhere to be found - and it highlights how locked-in I've been with the Symfony fauna. Configurations in Laravel are made within PHP-files, as well as .env. When I look over the configuration PHP-files they seem to serve the same purpose as the *services.yaml* file does in Symfony - a way to make environmental variables available to the app in a predicitive manner. But then again, there's also a *services.php* file for Laravel. And while on this subject - Symfony allows for configuration without any YAML if you prefer it. There's not too much crazy and right now there's nothing I need to change except the title of the project. I'll call it **The Elphant Blog**.


### Outlining The Elephant Blog
My blog would require a few things: 
* authentication
* blog post entity
* blog post editor
* blog post listings view
* and the blog post view

Keeping it simple like this, I'm going to have the listings-view serve as the homepage as well. This is the plan, let's see if I can make it through.

### Quick-starting authentication
Browsing the Laravel homepage I find a starter-kit for authentication: *breezer*. I'll try it out and see if it suits my needs.

I run `composer require laravel/breeze --dev` from my regular Windows environment. This install this package as a development dependency. Besides PHP-dependencies, this comes bundled with a few npm-packages as well. There's TailwindCSS, Axios and Laravel-Mix to mention a few. So to get this in order I also need to run `npm install` and `npm run dev` to make a first developer build. I'm almost ready but the documents wants me to update the database schema so I run `php artisan migrate`. 

```bash
Illuminate\Database\QueryException

SQLSTATE[HY000] [2002] The requested address is not valid in its context (SQL: select * from infor
mation_schema.tables where table_schema = example_app and table_name = migrations and table_type = '
BASE TABLE')
```

🤔 That doesn't look good. So it would appear that I can't connect to the container for whatever reason. So what would be the solution? Googling shows that some people needed to update their .env with the correct database address: `DB_HOST:0.0.0.0`. Inspecting the MYSQL-container in Docker, this corresponds to the address being used. Running `migrate` again throws the **same error** at me. I did some back and forth, but finally I resolved to move into WSL and run migration through Laravel's *sail* tool. This has the Docker credentials in row: Moving into WSL with `wsl -d ubuntu` from my project folder I run `./vendor/bin/sail artisan migrate`.

> PS, this is provided you have installed PHP and PHP-extensions required in your WSL distro. I hadn't. So within WSL I ran: 
> * `sudo add-apt-repository ppa:ondrej/php` to get access to PHP 8.
> * `sudo apt install php8.0 libapache2-mod-php8.0 php8.0-mysql`

> PPS. Running commands through *sail* when you have Docker containers is similar to how I've been doing the same with Symfony and its CLI-tool.

✔ Finally I get access to *register* and *login* pages. Without having to code anything (but debugging my developer environment a bit, which is not the fault of Laravel).
![A default Register page is active.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xsi4vmvcjfhm7zktbnvi.png)
 
### Creating a Post-entity
Laravel comes with its CLI-tool **artisan**. I've already used it to make database migrations. I suspect that it has, as Symfony's CLI-tool has, a simple way to create entity models. I run `php artisan list` to see what commands are available. Under the key *make* I see something interesting:
```console
 make
  make:model           Create a new Eloquent model class
```

To see what I can do with it I run `php artisan make:model --help`. This shows me arguments and options. A required argument is the name for the model. I can also add options so a controller, factory and migration is created at the same time. Realizing now that migration would require access to a docker container I'm going to continue on by using *sail*. Let's create the model **Post**:
`./vendor/bin/sail artisan make:model Post -cfm`
> The option *c* is to create a Controller. *f* is for a Factory. *m* is for migration.

I now have a basic (*Eloquent**) model, which extends a Model-class. By default every model has columns in a database for auto-incremented primary key, datetime it was updated, and datetime it was created - all without having to be specified in the model.

> **Eloquent* is Laravel's Object Relational Mapper. It serves same purpose as Doctrine does for Symfony.

I want **Post** to have following fields:
* Title
* Summary
* Text
* Slug

So I though I should define these in the class at `./app/Models/Post.php`. But NO. That is not the Eloquent way! It handles its fields rather more dynamically. I can however add these fields and corresponding methods as annotations to make it easier on myself. (Thanks [dotNET](https://stackoverflow.com/questions/51745634/laravel-models-where-are-model-properties/51746287) for clarifying this on StackOverflow).

```php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

/**
 * App\Models\Post
 * 
 * @property int $id
 * @property string $title
 * @property \Illuminate\Support\Carbon|null $created_at
 * @property \Illuminate\Support\Carbon|null $updated_at
 * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Post whereId($value)
 * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Post whereTitle($value)
 * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Post whereCreatedAt($value)
 * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Post whereUpdatedAt($value)
 */
class Post extends Model
{
    use HasFactory;
    /**
     * The model's default values for attributes.
     *
     * @var array
     */
    protected $attributes = [
        'title' => '',
        'summary' => '',
        'slug' => '',
        'text' => ''
    ];

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = ['title', 'summary', 'text'];
}
```

I also need to update the migration-file before running it.
```php
// database\migrations\2021_10_24_072240_create_posts_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreatePostsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            // Added by default
            $table->id();

            // Add these function-calls to define the model:
            $table->string('title');
            $table->text('summary');
            $table->text('text');
            $table->string('slug');
            
            // Added by default
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('posts');
    }
}

```

I've created the model and modified the migration class so the database schema is correctly updated. I can now run the migration: `./vendor/bin/sail artisan migrate`

🐬The way Laravel and Eloquent handles models is a bit different from Symfony and Doctrine. In Symfony I'm used to define entities and through the CLI it will ask me of what each field is called and what type it is. This will then provide information for migration so no manual coding is necessary. My first impression is that the model for Post is simply a placeholder whereas its defining characteristics are handled by the migration-file.

Back to task at hand. The "entity" is done ✔

### Building a **world-class** simple editor
☝ WORLD-CLASS meaning a simple form with a few input fields, and ways to store and fetch the posts. So I start by writing up a form for Laravels templating engine *Blade*. It's reminiscent of Twig but it allows regular PHP in it. Having installed *Breeze* in a previous step, I have access to Tailwind CSS. For this reason I'll be scaffolding the form with the Tailwind utility classes.

```php

<!-- resources\views\posts\_post_form.blade.php -->

<?php $labelClass="flex flex-col w-100"; ?>

<form action="/posts" method="POST" class="p-6">
    <h3>Add a Blog Post</h3>
    <div class="py-8 flex flex-col gap-4 justify-center max-w-xl m-auto">
        <label class="{{ $labelClass }}Title
            <input type="text" name="title" required>
        </label>
        <label class="{{ $labelClass }}">Summary
            <textarea name="summary"></textarea>
        </label>
        <label class="{{ $labelClass }}">Text
            <textarea name="text" required></textarea>
        </label>
        <input class="mt-8 w-40 px-4 py-2 bg-green-600 text-white" type="submit" value="Submit">
    </div>
</form>
```

This is a partial template that I'll use from the admin dashboard.

```php
<!-- resources\views\dashboard.blade.php -->
<x-app-layout>
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            {{ __('Dashboard') }}
        </h2>
    </x-slot>

    <x-slot name="slot">
        <div class="py-4">
            <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
                <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
                    @include('posts._post_form')
                </div>
            </div>
        </div>
    </x-slot>
</x-app-layout>
```

Logging into the dashboard, after registering, I see this:
![Laravel's Breeze dashboard with a form for creatinga a post.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pejerh0lb4qf1oqikv2u.png)
 

Next up I need to create a controller to fetch these inputs and store them. Or rather, I need to update the controller that *artisan* already has made for me. Remember that this was created at the same time I created the Post-model. For a first draft I see the need for three methods to this controller:
1. A method to create and store a new post.
2. A method to return an array of posts for a listings page.
3. A method to return a single post for displaying it.

```php
<?php
// app\Http\Controllers\PostController.php

namespace App\Http\Controllers;

use App\Models\Post;
use App\Providers\RouteServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class PostController extends Controller
{
    
    /**
     * Return an array of posts to a template.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        $posts = Post::all();

        return view('posts/post_list', ['posts' => $posts]);
    }


    /**
     * Store a new post in the database.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        if (!Auth::check()) {
            return RouteServiceProvider::HOME;
        }

        $title = $request->input('title');
        $summary = $request->input('summary');
        $text = $request->input('text');

        $post = new Post();
        $post->title = $title;
        $post->summary = $summary;
        $post->text = $text;
        $post->save();

        return view('posts/post_view', 
            [
                'title' => $post->getAttribute("title"), 
                'text' => $post->getAttribute("text"), 
                'summary' => $post->getAttribute("summary")
            ]
        );
    }

    /**
     * Return a post to a template if one is found.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function view(Request $request)
    {
        $id = $request->input('id');
        $post = Post::where('id', $id)->first();

        if (!$post) {
            return Redirect('/posts');
        }

        return view('posts/post_view', ['post' => $post]);
    }
}
```

This controller has everything I need right now. Only someone authenticated may create a post and there are methods to retrieve posts in a simple manner. But I'm not quite done yet. I need to add routes for them.

Routes are added in a seperate file `routes\web.php`. I add the following lines:
```php
use App\Http\Controllers\PostController;

Route::post('/posts', [PostController::class, 'store']);
Route::get('/posts', [PostController::class, 'index']);
Route::get('/post', [PostController::class, 'view']);
```
I create a couple of posts and can see that everything is working fine. And that's how we get the **WORLD-CLASS** 👀 editor done ✔

### Listing eloquent posts
Half the work to list the posts are already done. I created a method in the *PostController* that handles this for me. That method returns a view that references a template called `post_list` and passes an array of posts to it. I'll create this now:

```php
<!-- resources\views\posts\post_list.blade.php -->
<x-app-layout>
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            Blog Posts
        </h2>
    </x-slot>
    <x-slot name="slot">
        <div class="sm:px-6 lg:px-8 pb-8 bg-white overflow-hidden shadow-sm">
            <div class="flex flex-col gap-4">
                @each('posts._post_list_item', $posts, 'post')
            </div>
        </div>
    </x-slot>
</x-app-layout>
```

This template references a *partial*:
```php
<!-- resources\views\posts\_post_list_item.blade.php -->
<div>
    <h4 class="font-semibold"><a href="/post?id={{ $post->id }}">{{ $post->title }}</a></h4>
    <time class="text-sm">{{ $post->created_at }}</time>
    <p>{{ $post->summary }}</p>
</div>
```

The listings page is ready at */posts* ✔
![The listings page shows the title and summary of each blog post](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ph21w2olmrw4lrvyue83.png)


### A *complete* blog
The final piece (disregarding any other fancy functionalities) is to add a blog post view. Again, half the work is already done. There exists a controller handling fetching and returning a view, and a route to it. So now it's time to add the template:

```php
<!-- resources\views\posts\post_view.blade.php -->
<x-app-layout>
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            {{ $post->title }}
        </h2>
        <time class="text-sm">{{ $post->created_at }}</time>
    </x-slot>
    <x-slot name="slot">
        <div class="sm:px-6 lg:px-8 pb-8 bg-white overflow-hidden shadow-sm">
            <div class="flex flex-col gap-4 max-w-xl">
                <p>{{ $post->text }}</p>
            </div>
        </div>
    </x-slot>
</x-app-layout>
```

It's pretty much the same template as the listings page, but this time I'm accessing the text-property instead of the summary. So the final post-view in its glorious form:

![A blog post displaying a title, time it was created and its text. It's really simple-looking.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sabsuw94cg2opxr23eij.png)
 

## <span id="impressions">My final first impressions 💡</span>
* The Laravel-way is further from the Symfony-way than I thought it would be. No YAML, views are sorted under resources, models can dictate which database to use, and I need to manually configure the migration-files.
* Eloquent is different from Doctrine, but still kind-of easy to use.
* Blade is either the best of two worlds, or the worst. I haven't made up my mind! The templating engine allows me to treat it as just the view. Or, I can spice it up with whole sections of PHP-procedures.
* The documentation was not as thorough as I expected. I thought models would just be the Laravel version of entities. But it's more an apples to oranges comparison - if I haven't completely missed the mark.
* In a weekend in my spare-time I could couble together a simple blogging site in Laravel. That says to me that there might be something to work with here. 

I've heard people mention that Symfony would be hard and Laravel easy in comparison to it. I don't agree. Not for something as small-scale as my little project here. But this is coming from having done Symfony for a while and not having done any Laravel at all.