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:
-
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
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.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 🤷♂️)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.Move into the newly created directory:
cd example-app
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.The Docker containers are all running. Just check out localhost and there's the proof.
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
.
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).
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
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'];
}
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');
}
}
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>
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>
Logging into the dashboard, after registering, I see this:
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:
- A method to create and store a new post.
- A method to return an array of posts for a listings page.
- 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]);
}
}
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']);
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>
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>
The listings page is ready at /posts ✔
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>
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:
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.
Top comments (0)