DEV Community

Anders Björkland
Anders Björkland

Posted on

First impression: Laravel

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 straight to a summary of my impressions.

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:

    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

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
Enter fullscreen mode Exit fullscreen mode

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.

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')
Enter fullscreen mode Exit fullscreen mode

🤔 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.

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:

 make
  make:model           Create a new Eloquent model class
Enter fullscreen mode Exit fullscreen mode

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 for clarifying this on StackOverflow).

<?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'];
}
Enter fullscreen mode Exit fullscreen mode

I also need to update the migration-file before running it.

// 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');
    }
}

Enter fullscreen mode Exit fullscreen mode

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.


<!-- 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>
Enter fullscreen mode Exit fullscreen mode

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

<!-- 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>
Enter fullscreen mode Exit fullscreen mode

Logging into the dashboard, after registering, I see this:
Laravel's Breeze dashboard with a form for creatinga a post.

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
// 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]);
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

use App\Http\Controllers\PostController;

Route::post('/posts', [PostController::class, 'store']);
Route::get('/posts', [PostController::class, 'index']);
Route::get('/post', [PostController::class, 'view']);
Enter fullscreen mode Exit fullscreen mode

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:

<!-- 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>
Enter fullscreen mode Exit fullscreen mode

This template references a partial:

<!-- 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>
Enter fullscreen mode Exit fullscreen mode

The listings page is ready at /posts
The listings page shows the title and summary of each blog post

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:

<!-- 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>
Enter fullscreen mode Exit fullscreen mode

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.

My final first impressions 💡

  • 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.

Oldest comments (0)