https://sinnbeck.dev/feed <![CDATA[Sinnbeck.dev]]> Sinnbeck.dev blog posts. 2022-09-13T14:00:20+00:00 <![CDATA[Rate limiting routes in Laravel - with tests]]> https://sinnbeck.dev/8 Rate limiting routes in Laravel - with tests

I just had to do some work on some rate limits on a few routes in Laravel and I could not find any resources on how to set it up, and test it properly.

Setting up the rate limit

Add the following code inside the configureRateLimiting() method in RouteServiceProvider.php

1RateLimiter::for('test', function (Request $request) {
2    return Limit::perMinute(10)->by($request->ip());
3});

Here we add a new rate limiter for use in routes and name it test. We set it to only allow 10 requests per minute, and track it by the clients ip address. The route can now be added to a route (or route group) as middleware.

1Route::get('/test', [TestController::class, 'index'])->middleware(['throttle:test']);

Testing that its active

Add a new Feature test, and add a new test

1public function test_rate_limit_is_active()
2{
3    $this->get('/test')
4        ->assertOk()
5        ->assertHeader('X-Ratelimit-Limit', 10)
6        ->assertHeader('X-Ratelimit-Remaining', 9);
7}

We first check that the response is ok, and then check if the header has the rate limiter limit (max attempts per minute) and the remaining count.

Next we can check if the remaining goes down by 1 for each request.

1public function test_rate_limit_decreases_remaining()
2{
3    for(range(1, 10) as $i) {
4        $this->get('/test')
5            ->assertOk()
6            ->assertHeader('X-Ratelimit-Remaining', 10 - $i);
7    }
8    $this->get('/test')
9        ->assertStatus(429)
10        ->assertHeader('Retry-After', 60);
11}

First we make 10 requests to the page, ensuring that remaining is decreased properly. We then finally check that we are refused access and cannot try again for 60 seconds.

Resetting attempts

If for some reason the rate limiter needs to reset on a proper request (for a page that uses signed URL's for instance) this can be a bit tricky to set up.

First let use add the signed middleware to url route to secure it

1Route::get('/test', [TestController::class, 'index'])->middleware(['throttle:test', 'signed']);

and a test

1public function test_signed_url_blocks()
2{
3    $this->get('/test')
4        ->assertForbidden()
5        ->assertHeader('X-Ratelimit-Remaining', 9);
6}

Now we just need to make sure that remaining attempts reset after actually getting to the route. Open the TestController and set it up like the following

1<?php
2 
3namespace App\Http\Controllers;
4 
5use Illuminate\Http\Request;
6use Illuminate\Support\Facades\RateLimiter;
7 
8class TestController extends Controller
9{
10    public function index(Request $request)
11    {
12        RateLimiter::clear(md5('test' . $request->ip()));
13 
14        return view('test');
15    }
16}

Notice here the RateLimiter::clear(md5('test' . $request->ip()));. This is what will reset the rate limit when the request goes through. Laravel does this by concatenating the rate limiter name, with the limit key set with ->by(). It then hash the string with md5.

1RateLimiter::for('test', function (Request $request) {
2    return Limit::perMinute(10)->by($request->ip());
3});

Lets test that as well

1public function test_ratelimit_resets()
2{
3    $this->get('/test')
4        ->assertForbidden()
5        ->assertHeader('X-Ratelimit-Remaining', 9);
6 
7    $this->withoutMiddleware(\Illuminate\Routing\Middleware\ValidateSignature::class)
8        ->get('/test')
9        ->assertOk()
10        ->assertHeader('X-Ratelimit-Remaining', 10);
11}

First we run a forbidden request to get the rate limiter to decrease. Next we do a secondary request where we disable the signature validation, and asserts that the remaining is back to 10.

The end

Hope that was helpful to some. As you can see, rate limit in laravel is simple to interact with once you get the basics. If you find any mistakes or have ideas for improvements, please contact me on @rsinnbeck

]]>
2022-09-13T14:00:20+00:00
<![CDATA[Getting Vite and Laravel to work with Lando]]> https://sinnbeck.dev/7 Getting Vite and Laravel to work with Lando

I am writing the blog post as I keep getting contacted by people who want to know how how I got Vite to work with Lando. This guide will work with both running your site with http and https.

Set up Lando

First we add a new service to .lando.yml for running node

1services:
2 node:
3    type: node:16
4    scanner: false
5    ports:
6     - 3009:3009
7    build:
8     - npm install

Here we install node version 16, and open port 3009.

We will also add two commands for running the dev server and building assets.

1tooling:
2 dev:
3    service: node
4    cmd: npm run dev
5 build:
6    service: node
7    cmd: npm run build

Rebuild your site with lando rebuild -y and the new service is ready.

Configure vite

Open the vite.config.js file that came with Laravel and add this server config

1import { defineConfig } from 'vite';
2import laravel from 'laravel-vite-plugin';
3 
4export default defineConfig({
5    plugins: [
6        laravel({
7            input: ['resources/css/app.css', 'resources/js/app.js'],
8            refresh: true,
9        }),
10    ],
11    server: {
12        https: false,
13        host: true,
14        port: 3009,
15        hmr: {host: 'localhost', protocol: 'ws'},
16    },
17});

The trick is that we set the dev server to run on http + ws. Normally this wouldnt work with a https site, but as we also set the dev server to be accessed on localhost, the browser will allow it anyways!

That's it!

Start the Vite server with lando dev and open your site like your normally do. Hot reloading should now be working!

Any problems or comments, feel free to reach out to me on @rsinnbeck

]]>
2022-09-05T14:00:20+00:00
<![CDATA[Making a complete file uploader with progressbar using Livewire and Alpinejs]]> https://sinnbeck.dev/6 Making a complete file uploader with progressbar using Livewire and Alpinejs

I recently had to create a file uploader that allowed the user to both click a button and select files, or simply drag files unto the page. I decided to give Livewire a shot as it seemed interesting. My first thought was to use a js package like resumeable.js or DropZone, but after reading Livewires file upload docs, I figured it might be easier to just use the built in system and extend it with some simple JavaScript.

Preview

You can see a demo of the final code here

This guide assumes that you have already installed the latest laravel and livewire 2.x.

Making the upload component

First lets create the livewire component. Run this in your terminal

php livewire:make FileUploader

This should give you two new files. /app/Http/Livewire/FileUploader.php and /resources/views/livewire/file-uploader.blade.php

Designing the uploader

For designing layouts I prefer to use TailwindCss, so I will use that throughout this guide. But feel free to replace it with whatever css framework (or custom css) that you like.

For simplicity I am making this as a full page component, but it should work inside a blade view as well. Just remember to have both livewire scripts and the alpine cdn included in the layout.

Add a file in /resources/views/layouts/app.blade.php with this content

1<head>
2    @livewireStyles
3    <script defer src="https://unpkg.com/[email protected]/dist/cdn.min.js"></script>
4</head>
5<body>
6    {{ $slot }}
7 
8    @livewireScripts
9</body>

Note that I am using the tailwind play cdn. I recommend replacing this with tailwind that is actually compiled with either vite or laravel mix.

Add this routes to web.php

1Route::get('file-uploader', \App\Http\Livewire\FileUploader::class)

Open /resources/views/livewire/file-uploader.blade.php and paste this html into it

1<div>
2    <div class="flex flex-col items-center justify-center h-screen bg-slate-200">
3        <label class="flex flex-col items-center justify-center w-1/2 bg-white border shadow cursor-pointer h-1/2 rounded-2xl hover:bg-slate-50" for="file-upload"
4 >
5            <h3 class="text-3xl">Click here to select files to upload</h3>
6            <em class="italic text-slate-400">(Or drag files to the page)</em>
7        </label>
8        <input type="file" id="file-upload" multiple class="hidden" />
9    </div>
10</div>

This will give us a simple layout with a huge "button" that lets us click it to select the files to upload. This works by using a label as the container, and linking it to a hidden file input.

Getting upload working

To make our code a bit cleaner we will add a script tag inside the page to set up the data for alpine first. It will just return an object with some defaults we will use later. Add x-data="fileUpload()" to the outermost div, and add the script shown below. Be sure to nest it inside the outermost div, as livewire does not like multiple root elements.

1<div x-data="fileUpload()">
2    <div class="flex flex-col items-center justify-center h-screen bg-slate-200">
3        <label class="flex flex-col items-center justify-center w-1/2 bg-white border shadow cursor-pointer h-1/2 rounded-2xl hover:bg-slate-50" for="file-upload"
4        >
5            <h3 class="text-3xl">Click here to select files to upload</h3>
6            <em class="italic text-slate-400">(Or drag files to the page)</em>
7        </label>
8        <input type="file" id="file-upload" multiple class="hidden" />
9        </div>
10    
11    <script>
12    function fileUpload() {
13        return {
14            isDropping: false,
15            isUploading: false,
16            progress: 0,
17        }
18    }
19    </script>
20</div>

Normally when you add a file input to the page with livewire, you would simply use wire:model to automatically upload the files as they are added (to a temp directory). But as we need to implement the upload when dragging files, it makes sense to implement this ourselves as well. Luckily livewire makes this very simple.

Add a new function inside the returned object in the script

1<script>
2    function fileUpload() {
3        return {
4            isDropping: false,
5            isUploading: false,
6            progress: 0,
7            handleFileSelect(event) {
8                if (event.target.files.length) {
9                    console.log(event.target.files)
10                }
11            },
12        }
13    }
14</script>

and add the script to the file inputs @change hook

1<input type="file" id="file-upload" multiple @change="handleFileSelect" class="hidden" />

Now try clicking the button and select a file. Open the browser console (F12) and you should see a log of the file being added.

Now for the actual upload.

First we will add the WithFileUploads trait to the component to tell livewire that we want file uploads. Next we add a new public property to the component class to have a place for livewire to store a reference to the files.

1<?php
2 
3namespace App\Http\Livewire;
4 
5use Livewire\Component;
6use Livewire\WithFileUploads;
7 
8class FileUploader extends Component
9{
10    use WithFileUploads;
11 
12    public $files = [];
13 
14    public function render()
15    {
16        return view('livewire.file-uploader');
17    }
18}

Livewire has a few handle built in helpers for uploading files which we can use. https://laravel-livewire.com/docs/2.x/file-uploads#js-api

So we add a new method to handle all uploads called uploadFiles(). It receives the files and uses the livewire @this.uploadMultiple() function to upload them to the livewire component. Note that the first argument is the name of the public property in the component class. Not that I am rebinding this to $this. This is because the this changes when inside the functions of each callback in the upload. This could also be handled by using arrow functions

1<script>
2    function fileUpload() {
3        return {
4            isDropping: false,
5            isUploading: false,
6            progress: 0,
7            handleFileSelect(event) {
8                if (event.target.files.length) {
9                    this.uploadFiles(event.target.files)
10                }
11            },
12            uploadFiles(files) {
13                const $this = this
14                this.isUploading = true
15                @this.uploadMultiple('files', files,
16                    function (success) { //upload was a success and was finished
17                        $this.isUploading = false
18                        $this.progress = 0
19                    },
20                    function(error) { //an error occured
21                        console.log('error', error)
22                    },
23                    function (event) { //upload progress was made
24                        $this.progress = event.detail.progress
25                    }
26                )
27            }
28        }
29    }
30</script>

First we set the upload to have started with this.isUploading = true. We then update the other attributes based on the upload status.

Now before we test it out, it might be nice to be able to see the files, so let us update the view to show them. Lets add some code right after the <label> to show the files

1<label class="flex flex-col items-center justify-center w-1/2 bg-white border shadow cursor-pointer h-1/2 rounded-2xl hover:bg-slate-50"
2    for="file-upload"
3>
4    <h3 class="text-3xl">Click here to select files to upload</h3>
5    <em class="italic text-slate-400">(Or drag files to the page)</em>
6</label>
7 @if(count($files))
8    <ul class="mt-5 list-disc">
9        @foreach($files as $file)
10            <li>{{$file->getClientOriginalName()}}</li>
11        @endforeach
12    </ul>
13@endif

Now try selecting a file again, and this time you should see the file names show up in a list.

Dropping files

Dropping of files is actually quite easy to implement now that we have the regular upload working. First we will add a function similar to the one used for file selection.

1<script>
2    function fileUpload() {
3        return {
4            isDropping: false,
5            isUploading: false,
6            progress: 0,
7            handleFileSelect(event) {
8                if (event.target.files.length) {
9                    this.uploadFiles(event.target.files)
10                }
11            },
12            handleFileDrop(event) {
13                if (event.dataTransfer.files.length > 0) {
14                    this.uploadFiles(event.dataTransfer.files)
15                }
16            },
17            uploadFiles(files) {
18                const $this = this;
19                this.isUploading = true
20                @this.uploadMultiple('files', files,
21                    function (success) {
22                        $this.isUploading = false
23                        $this.progress = 0
24                    },
25                    function(error) {
26                        console.log('error', error)
27                    },
28                    function (event) {
29                        $this.progress = event.detail.progress
30                    }
31                )
32            }
33        }
34    }
35</script>

Next we will let the whole page accept file drops. Luckily AlpineJs has built in checks for file drops, so it is very easy add.

1<div x-data="fileUpload()">
2    <div class="flex flex-col items-center justify-center h-screen bg-slate-200"
3         x-on:drop="isDroppingFile = false"
4         x-on:drop.prevent="handleFileDrop($event)"
5         x-on:dragover.prevent="isDroppingFile = true"
6         x-on:dragleave.prevent="isDroppingFile = false"
7    >

Now drag a file onto the page, and it should just work!

For the sake of completion, lets just add a quick overlay on the page so its obvious we are inside the drop zone.

1<div x-data="fileUpload()">
2    <div class="relative flex flex-col items-center justify-center h-screen bg-slate-200"
3         x-on:drop="isDropping = false"
4         x-on:drop.prevent="handleFileDrop($event)"
5         x-on:dragover.prevent="isDropping = true"
6         x-on:dragleave.prevent="isDropping = false"
7    >
8        <div class="absolute top-0 bottom-0 left-0 right-0 z-30 flex items-center justify-center bg-blue-500 opacity-90"
9             x-show="isDropping"
10        >
11            <span class="text-3xl text-white">Release file to upload!</span>
12        </div>

Here we add relative to the div that covers the page, and adds a simple overlay div that is shown whenever we are dragging over the page.

I am aware that there is some flickering when the file is right on top of the text in the middle of the page. Sadly I have yet to find a solution for this. Let me know if you find a solution so I can update the guide. @rsinnbeck

Adding a progress bar

To complete our file uploader, I think it would be great with a progress bar.

At the bottom of our <label> we can add a small bar that gets filled whenever we upload anything.

1<label class="flex flex-col items-center justify-center w-1/2 bg-white border shadow cursor-pointer select-none h-1/2 rounded-2xl hover:bg-slate-50"
2    for="file-upload"
3>
4    <h3 class="text-3xl">Click here to select files to upload</h3>
5    <em class="italic text-slate-400">(Or drag files to the page)</em>
6    <div class="bg-gray-200 h-[2px] w-1/2 mt-3">
7        <div
8            class="bg-blue-500 h-[2px]"
9            style="transition: width 1s"
10            :style="`width: ${progress}%;`"
11            x-show="isUploading"
12        >
13        </div>
14    </div>
15    
16</label>

And just like that we can see the progress of the uploads! Be aware that you might not be able to see the progress bar move at all, as the uploads are so fast when working locally. Therefor it might be a good idea to enable throttling in the browser, so you can actually see it progress: https://www.browserstack.com/guide/how-to-perform-network-throttling-in-chrome

Append uploads

You might notice that if you add files in more than one go, it will replace all the files from earlier. This is due to the fact that, livewire just throws away old files when using @this.uploadMultiple(). Luckily we can just overwrite the upload function for this component to force it to merge the uploads.

Add a finishUpload() method to the component class

1<?php
2 
3namespace App\Http\Livewire;
4 
5use Livewire\Component;
6use Livewire\TemporaryUploadedFile;
7use Livewire\WithFileUploads;
8 
9class FileUploader extends Component
10{
11    use WithFileUploads;
12 
13    public $files = [];
14 
15    public function finishUpload($name, $tmpPath, $isMultiple)
16    {
17        $this->cleanupOldUploads();
18 
19        $files = collect($tmpPath)->map(function ($i) {
20            return TemporaryUploadedFile::createFromLivewire($i);
21        })->toArray();
22        $this->emitSelf('upload:finished', $name, collect($files)->map->getFilename()->toArray());
23 
24        $files = array_merge($this->getPropertyValue($name), $files);
25        $this->syncInput($name, $files);
26    }
27 
28    public function render()
29    {
30        return view('livewire.file-uploader');
31    }
32}

And that should be it! Now you can just keep adding files without them being removed. This "fix" is courtesy of this issue I found on Livewires github

Removing uploaded files before submit

As the files are only uploaded to a temp folder, you of course want to submit the form to finally save them. But perhaps the user added a file by accident and want to remove it without starting over. Let's add a remove button!

1@if(count($files))
2 <ul class="mt-5 list-disc">
3        @foreach($files as $file)
4 <li>
5                {{$file->getClientOriginalName()}}
6 <button class="text-red-500" @click="removeUpload('{{$file->getFilename()}}')">X</button>
7            </li>
8        @endforeach
9 </ul>
10@endif

When the user clicks it, it will call a function in AlpineJs that does the removal

1<script>
2    function fileUpload() {
3        return {
4            isDropping: false,
5            isUploading: false,
6            progress: 0,
7            handleFileSelect(event) {
8                if (event.target.files.length) {
9                    this.uploadFiles(event.target.files)
10                }
11            },
12            handleFileDrop(event) {
13                if (event.dataTransfer.files.length > 0) {
14                    this.uploadFiles(event.dataTransfer.files)
15                }
16            },
17            uploadFiles(files) {
18                const $this = this;
19                this.isUploading = true
20                @this.uploadMultiple('files', files,
21                    function (success) {
22                        $this.isUploading = false
23                        $this.progress = 0
24                    },
25                    function(error) {
26                        console.log('error', error)
27                    },
28                    function (event) {
29                        $this.progress = event.detail.progress
30                    }
31                )
32            },
33            removeUpload(filename) {
34                @this.removeUpload('files', filename)
35            },
36        }
37    }
38</script>

This again just uses a built-in livewire method to remove the file. Now you just need to add validation and the form submission yourself.

The end

I hope that this guide helped you build something great. Feel free to reach out if you feel this guide is missing something or has any mistakes. @rsinnbeck

]]>
2022-09-05T14:00:20+00:00
<![CDATA[Adding bound toSql() to laravel]]> https://sinnbeck.dev/5 Adding bound toSql() to laravel

Often when debugging I find myself using ->toSql() to get the SQL query string used by the query builder, to see if it looks correct. Sadly this does not work when the query includes parameters as these are only used for binding in PDO.

It would be nice to be able to get the query with the parameters as well, so let's add that to laravel!

But how?!

Luckily laravel supports macros, which lets us easily extend various built in classes in laravel, without having to edit the vendor source files (Which can be overwritten every time we run composer update).

You can add a macro inside a service providers boot method. It works by calling the static macro() method on the class we wish to extend. The first parameter is the new method name, and the second parameter is a callback method that we call as if it was a native method on the underlying class. Since it is being run from the class, we have access to $this inside the callback!

Add a service provider

First add a service provider that can hold the new macros.

php artisan make:provider MacroServiceProvider

This will give you a clean service provider like this

1<?php
2 
3namespace App\Providers;
4 
5use Illuminate\Support\ServiceProvider;
6 
7class MacroServiceProvider extends ServiceProvider
8{
9    /**
10     * Register services.
11     *
12     * @return void
13     */
14    public function register()
15    {
16        //
17    }
18 
19    /**
20     * Bootstrap services.
21     *
22     * @return void
23     */
24    public function boot()
25    {
26        //
27    }
28}

Add the macro

We delete the register method since we don't need it, and add the following code to the boot method

1Builder::macro('toBoundSql', function () {
2    /* @var Builder $this */
3    $bindings = array_map(
4        fn ($parameter) => is_string($parameter) ? "'$parameter'" : $parameter,
5        $this->getBindings()
6    );
7 
8    return Str::replaceArray(
9        '?',
10        $bindings,
11        $this->toSql()
12    );
13});
14 
15EloquentBuilder::macro('toBoundSql', function () {
16    return $this->toBase()->toBoundSql();
17});

The end result should look like this.

1<?php
2 
3namespace App\Providers;
4 
5use Illuminate\Support\Str;
6use Illuminate\Database\Query\Builder;
7use Illuminate\Support\ServiceProvider;
8use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
9 
10class MacroServiceProvider extends ServiceProvider
11{
12    /**
13     * Bootstrap services.
14     *
15     * @return void
16     */
17    public function boot()
18    {
19        Builder::macro('toBoundSql', function () {
20            /* @var Builder $this */
21            $bindings = array_map(
22                fn ($parameter) => is_string($parameter) ? "'$parameter'" : $parameter,
23                $this->getBindings()
24            );
25 
26            return Str::replaceArray(
27                '?',
28                $bindings,
29                $this->toSql()
30            );
31        });
32 
33        EloquentBuilder::macro('toBoundSql', function () {
34            return $this->toBase()->toBoundSql();
35        });
36 
37    }
38}

There are two macros with the same name. The first is for binding the method to the DB query builder, that binding is what makes all of this work. However, we are not quite there yet, this won't work with eloquent as it will just return the string to eloquent. That means that we will output the eloquent builder class object, instead of a string. To fix this, we add the second macro, which will bind directly to eloquent. All it does is get the string by calling the underlying query builder, and return the result as a string.

Let's try it out!

To test it out, you can add the following

This is what it looks like without the new macros.

1dd(\App\Models\User::where('id', '>'. 100)->latest()->toSql());

It gives us this SQL.

1select * from `users` where `id` = ? and `users`.`deleted_at` is null order by `created_at` desc

After we added the new macros.

1dd(\App\Models\User::where('id', '>', 100)->latest()->toBoundSql());

This should give us

1select * from `users` where `id` > 100 and `users`.`deleted_at` is null order by `created_at` desc

Or we can try the DB version

1dd(\DB::table('users')->where('id', '>', 100)->latest()->toBoundSql());

Which will give us

1select * from `users` where `id` > 100 order by `created_at` desc

Conclusion

As you can see adding new methods to laravel is quite easy, and getting a better SQL output only takes a few lines of code. Even though packages like Debugbar or Clockwork handle this already, I personally like being able to get the raw query inside of tinker or a command.

]]>
2022-02-28T14:00:20+00:00
<![CDATA[Using lando to run laravel in docker]]> https://sinnbeck.dev/4 Using lando to run laravel in docker

Laravel comes with laravel sail, which is great! Personally I prefer using Lando as I find it gives me alot more features, like tooling, proxy and events.

I suggest looking through the docs to see if lando is for you!

This tutorial assumes that you have already installed lando and docker by following the guide on the website

You do not need php or composer installed locally to follow this guide!

Set up a laravel project!

First we will initialize a new laravel project.

If you already have an existing project, just go into the root of the project and run lando init and follow the guide. After that you can skip to the next section!

Go to the root of where you store your web projects. Now run the following command. Be sure to replace my-app before you run it! The name cannot contain spaces!

SITENAME=my-app; docker run --rm --interactive --tty \
--volume $PWD:/app \
--user $(id -u):$(id -g) \
composer create-project laravel/laravel $SITENAME ^9.0 \
&& cd $SITENAME \
&& lando init --source cwd --webroot ./public --recipe laravel --name $SITENAME

This is currently untested on Windows, so if it works/fails, feel free to send me a message on twitter at @rsinnbeck

When the command has completed, you will now have a brand new laravel project with the name you provided. In the root of the new directory you will find a file named .lando.yml. This is the config file generated by lando.

It will look something like this

1name: my-app
2recipe: laravel
3config:
4 webroot: ./public

These lines mean the following:

  • name: The name lando uses to reference the site. This will also be prefix for the sitename when we open it in the browser later on!
  • recipe: Lando uses recipes to make setting up a new project really fast. Here we are using the laravel recipe config: Here we can pass config values to the recipe. We have set it to use the ./public directory to serve files

Configure the docker containers

Lets add some more items to our config so it resembles more what we normally use with laravel

1name: my-app
2recipe: laravel
3config:
4 webroot: ./public
5 php: '8.0'
6 via: apache #or nginx
7 database: mysql #or mariadb or postgres
8 cache: redis #or memcached

Here we are setting which php version to use and we are telling lando which webserver, database and cache-server to install. All the containers can be suffixed with a version by using eg. :2.0

Next set up your .env file with the correct values. If you are using postgres, the database config is a bit different. Refer to the docs for the correct values.

1DB_CONNECTION=mysql
2DB_HOST=database
3DB_PORT=3306
4DB_DATABASE=laravel
5DB_USERNAME=laravel
6DB_PASSWORD=laravel
7 
8REDIS_HOST=cache

Time to try it out

Now it's time to start our new app! Simply run

lando start

Lando will now download the correct containers and start them all up. If all went well you should see a message looking something like this

1Your app has started up correctly.
2Here are some vitals:
3 
4 NAME            my-app
5 LOCATION        /path/to/my-app
6 SERVICES        appserver, database
7 APPSERVER URLS https://localhost:49548
8                 http://localhost:49549
9                 http://my-app.lndo.site/
10                 https://my-app.lndo.site/

Your ports are probably different that above, but that is just because lando will find a random port on each run. Notice the services. These are the names of the containers we have built. The appserver is the main container used for running php!

You can now try opening one of the http:// links, and you should see the laravel welcome page!

Using laravel through lando

So the first thing we want to do is to make sure that all migrations are run. To run artisan commands we just replace php with lando, and it will run artisan inside the php container.

lando artisan migrate

Try out a new more artisan command to see that it actually works just as running it locally!

Or you can use composer

lando composer

We can also run arbitrary php commands in the same way

lando php -v

This should give you the php version number running inside the container

We can even jump right into the php container by running

lando ssh

You are now inside the container, and can run any command as if you were on your own computer. Type exit and his enter to get back to your own machine.

Tooling

Running migrations is something we need to do quite often, so let us make a handy shortcut for it!

Add the following to your .lando.yml file

1name: my-app
2recipe: laravel
3config:
4 webroot: ./public
5 php: '8.0'
6 via: apache
7 database: mysql
8 cache: redis
9tooling:
10 migrate:
11    service:
12    cmd: php artisan migrate

We are adding a new command (tool) called migrate. It will be run on the service named appserver which is the one running php. The command it will run is php artisan migrate

Now try running

lando migrate

As you can see it works, and we can now shorten the command just a bit. Feel free to add your own commands to make your life a little easier.

You can always get a full list of available commands by running lando

Add a container for running laravel mix

In case you want to run laravel mix (or similar) we need a container for that. Add the following to your .lando.yml file

1name: my-app
2recipe: laravel
3config:
4 webroot: ./public
5 php: '8.0'
6 via: apache
7 database: mysql
8 cache: redis
9services:
10 node:
11    type: node:16
12    scanner: false
13    build:
14     - npm install
15tooling:
16 migrate:
17    service: appserver
18    cmd: php artisan migrate

Here we are telling lando to add an extra service, named node.

We want it to install node version 16. Lando will automatically try scanning the new service to see if it can connect to it using http. As we do not want this, we will disable the scanner.

Lastly we will tell it to run npm install as soon as it is has added the new service (just so we don't forget)

Run lando rebuild -y to rebuild all the services, and add the new node service. As all the previous containers already exist and haven't changed, it should be pretty fast.

Like we did with the migrate command earlier we will now also add a command for running npm Add the following to your .lando.yml file under the tooling: section

1name: my-app
2recipe: laravel
3config:
4 webroot: ./public
5 php: '8.0'
6 via: apache
7 database: mysql
8 cache: redis
9services:
10 node:
11    type: node:16
12    scanner: false
13    build:
14     - npm install
15tooling:
16 migrate:
17    service: appserver
18    cmd: php artisan migrate
19 npm:
20    service: node
21    cmd: npm

Now run lando npm run dev to confirm that it's working

Add mailhog to catch mails

Let us also add mailhog, to make sure we don't accidentially send emails to our clients

First add the service

1name: my-app
2recipe: laravel
3config:
4 webroot: ./public
5 php: '8.0'
6 via: apache
7 database: mysql
8 cache: redis
9services:
10 node:
11    type: node:16
12    scanner: false
13    build:
14     - npm install
15 mail:
16    type: mailhog
17    portforward: true
18    hogfrom:
19     - appserver
20tooling:
21 migrate:
22    service: appserver
23    cmd: php artisan migrate
24 npm:
25    service: node
26    cmd: npm

Here we add the mailhog service with the name mail. We want to let the scanner check this service as it actually has a webinterface, and we tell lando to make sure that all http requests are directed to the container. We also tell mailhog that it should grab mails from the appserver container.

Run lando rebuild -y to start the service.

Once the service is started, lando will show you an url on which you can see the mailhog interface. Here all emails from laravel will end up.

To make sure it works, change your .env file to use the new mail service

1MAIL_DRIVER=smtp
2MAIL_HOST=mail
3MAIL_PORT=1025
4MAIL_USERNAME=null
5MAIL_PASSWORD=null

Now when we send emails from laravel, they will be caught by mailhog.

Proxy

Before we wrap things up lets just add a proxy to the mailhog service, to make using it a bit easier.

Add the following to the bottom of your .lando.yml file (replace my-app with your actual project name)

1name: my-app
2recipe: laravel
3config:
4 webroot: ./public
5 php: '8.0'
6 via: apache
7 database: mysql
8 cache: redis
9services:
10 node:
11    type: node:16
12    scanner: false
13    build:
14     - npm install
15 mail:
16    type: mailhog
17    portforward: true
18    hogfrom:
19     - appserver
20tooling:
21 migrate:
22    service: appserver
23    cmd: php artisan migrate
24 npm:
25    service: node
26    cmd: npm
27proxy:
28 mail:
29    - mail.my-app.lndo.site

Run lando rebuild -y and when it is done you will see that lando now have assigned the the mail service to the provided url. This makes opening it in the future alot easier, as we can bookmark the url.

Wrap-up

We have now successfully set up a new laravel app using lando. I hope it was easy to follow and that you will be using lando in the future.

If you want more lando tutorials, you can send me a message on Twitter at @rsinnbeck

]]>
2022-02-18T14:00:20+00:00
<![CDATA[Laravel groupBy() error]]> https://sinnbeck.dev/3 Laravel groupBy() error

Helping out at the laracasts forum, I often see people running into issues with using groupBy() in queries. If you are running into errors like this

1SQLSTATE[42000]: Syntax error or access violation:
2 1055 Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated colum

.. then you are most likely the target of this blog post. Be aware that this post isn't meant to teach mysql GROUP BY, but rather give you a simple understanding of why the query is failing.

The error often comes from a query like this

1$posts = Post::query()
2    ->groupBy('category_id')
3    ->get();

Now the idea is most likely that you just want to group the posts into their respective categories with all columns. But that is now how it works on a database level.

To make it easier to understand, lets use a spreadsheet as an example instead

id name category_id author_id visits
1 Post 1 1 4 32
2 Post 2 1 8 12
3 Post 3 2 12 201
4 Post 4 2 4 3

The database will always return rows, so let us try and run the above query on the data set. Open a spreadsheet a try to make two rows with all data. It is two rows as category_id can either be 1 or 2. So which id should it pick? 1 or 2? and the name, slug or even owner?

id name category_id author_id visits
1 or 2 Post 1 or Post 2 1 4 or 8 32 or 12
3 or 4 Post 3 or Post 4 2 12 or 4 201 or 3

Are you starting to see the problem?

"So how can I fix it?"

The simplest way is to only get only the columns we are grouping by, in this case category_id

1$posts = Post::query()
2    ->select('category_id')
3    ->groupBy('category_id')
4    ->get();
category_id
1
2

While this works, maybe it isn't actually what we want. So now we need to plan out exactly what we want for each column. Perhaps we want to know what category id has the most visits. Here we can use what is called an aggregate function.

1$posts = Post::query()
2    ->select('category_id', \DB::raw('MAX(visits) as max_visits'))
3    ->groupBy('category_id')
4    ->get();

We are using a raw query as there isn't anyway to specify the MAX function in a select in laravel. Also we are aliasing it to max_visits to make it easy to reference in laravel.

category_id max_visits
1 32
2 201

"But the query works in my database manager!"

You might indeed experience that the raw group by query actually works in your database manager. This is because laravel runs SQL queries in "strict mode", while your database manager does not. It is possible to simply disable "strict mode" in laravel, by setting it to false in the database.php config file. While possible I cannot recommend doing so. It is better to spend the time learning how to write proper SQL queries, as the results given by turning "strict mode" off, can be unpredictable and lead to problems down the road.

"But I want all columns!"

Okay so you want to get all columns. Then the trick is to simply not use groupBy() on a database level. Instead you can use it with the returned collection instead. This will group the posts by the category it belongs to as expected (a nested collection)

1$posts = Post::query()
2    ->get()
3    ->groupBy('category_id');

This is result in a structure like (here shown as a php array to make it easier to read)

1[
2 
3'1' => [
4    ['id' => 1, 'name' => 'Post 1', 'category_id' => 1, 'author_id' => 4 'visits' => 32],
5    ['id' => 2, 'name' => 'Post 2', 'category_id' => 1, 'author_id' => 8 'visits' => 12],
6],
7'2' => [
8    ['id' => 3, 'name' => 'Post 3', 'category_id' => 2, 'author_id' => 12 'visits' => 201],
9    ['id' => 4, 'name' => 'Post 4', 'category_id' => 2, 'author_id' => 4 'visits' => 0],
10],

Wrap up

So I hope this gave you some idea as to why your query is failing. If not, try asking at https://laracasts.com/discuss

]]>
2022-02-17T14:00:20+00:00
<![CDATA[Launch of a new blog]]> https://sinnbeck.dev/1 Launch of a new blog

I have for a long time considered making a blog, but just never got around to it. So after my good friend Tray2 made his blog, I figured it was about time.

I hope to find some good topics over time. Most topics I write will be to help out people over at https://laracasts.com/discuss. Often I see people struggle with the same issues over and over, so having some guides that people can just link to, might be helpful!

]]>
2021-12-16T14:00:20+00:00
<![CDATA[Facades are singletons 🤯]]> https://sinnbeck.dev/2 Facades are singletons 🤯

If you know a little about the service container, you also may know that you can explicitly bind things in the container as a singleton. What you might not know, is that they all actually are singletons.

Before we begin, let's quickly go through how the service container works.

NB: All code examples uses PHP 8 syntax

How the service container works

In laravel you often inject classes in controllers like this

1public function show(MyAwesomeClass $awesome): View
2{
3}

For this to work laravel needs to know how to resolve the class MyAwesomeClass. If the MyAwesomeClass constructor does not take any parameters or they are all resolvable using the container then laravel will just magically new up a class instance. For the sake of argument let's say that our class constructor requires a parameter to create a new instance of the class.

1<?php
2namespace App\Foobar;
3 
4class MyAwesomeClass
5{
6    public $name;
7 
8    public function __construct(string $name): void
9    {
10        $this->name = $name;
11    }
12 
13    public function setName(string $name): self
14    {
15        $this->name = $name;
16        return $this;
17    }
18}

We would then need to set this up in the container. This can be done as follows in a service provider.

1$this->app->bind(MyAwesomeClass::class, function ($app) {
2    return new MyAwesomeClass('foo');
3});

Here we tell the service container that it should new up an instance of the class with the parameter $name = 'foo' if it is requested from the container.

The service container will always make a new instance of the class each time we call it. This might not always be what we want. In some cases it might make more sense to always get the same exact instance. Examples of this can be the Request or Session class, as we want to be able to change these in one part of our app, and have the change reflected elsewhere (like changing the request in a middleware). We can easily change our binding from before so that it always returns the same MyAwesomeClass instance

1$this->app->singleton(MyAwesomeClass::class, function ($app) {
2    return new MyAwesomeClass(['foo' => 'bar']);
3});

A quick recap. With the first example we will get a new class instance of the MyAwesomeClass each time.

1$class1 = app(MyAwesomeClass::class);
2$class1->setName('Foobar');
3$class2 = app(MyAwesomeClass::class);
4 
5echo $class1->name; //shows Foobar
6echo $class2->name; //shows foo

And with the singleton we get the same class instance each time

1$class1 = app(MyAwesomeClass::class);
2$class1->setName('Foobar')
3$class2 = app(MyAwesomeClass::class);
4 
5echo $class1->name; //shows Foobar
6echo $class2->name; //shows Foobar

How facades work

So now that you understand the basics of how the service container works, let's see how a Facade works. A facade has just one method: getFacadeAccessor()

1<?php
2namespace App\Facades;
3 
4use Illuminate\Support\Facades\Facade;
5 
6class MyAwesomeClassFacade extends Facade
7{
8    /**
9     * Get the registered name of the component.
10     *
11     * @return string
12     */
13    protected static function getFacadeAccessor() {
14        return \App\Foobar\MyAwesomeClass::class;
15    }
16}

Here we tell the facade that it is uses the MyAwesomeClass from before. That means that when we use the facade, it knows that it should return a MyAwesomeClass instance.

1$myclass = MyAwesomeClassFacade::setName('foobar');

This will make a new instance of MyAwesomeClass and then automatically run the setName()method. This is done using the __callStatic()method in the base Facade class.

1public static function __callStatic($method, $args)
2{
3    $instance = static::getFacadeRoot();
4 
5    if (! $instance) {
6        throw new RuntimeException('A facade root has not been set.');
7    }
8 
9    return $instance->$method(...$args);
10}

The getFacadeRoot() method calls the resolveFacadeInstance() method that is in charge of getting the correct class instance from the container.

Why facades return the same class instance again and again (singleton)

I hope you now have a basic understanding of how classes are resolved in laravel. No let's see why facades are indeed singletons. As I said earlier, the resolveFacadeInstance() method is responsible for getting the correct class from the service container. Let's now have a look at what it does.

1protected static function resolveFacadeInstance($name)
2{
3    if (is_object($name)) {
4        return $name;
5    }
6 
7    if (isset(static::$resolvedInstance[$name])) {
8        return static::$resolvedInstance[$name];
9    }
10 
11    if (static::$app) {
12        return static::$resolvedInstance[$name] = static::$app[$name];
13    }
14}

Let's break it down further, and we will start at the end.

1protected static function resolveFacadeInstance($name)
2{
3    if (is_object($name)) {
4        return $name;
5    }
6 
7    if (isset(static::$resolvedInstance[$name])) {
8        return static::$resolvedInstance[$name];
9    }
10 
11    if (static::$app) {
12        return static::$resolvedInstance[$name] = static::$app[$name];
13    }
14}

The very last part of this code is what actually gets the instance back from the container. static::$app[$name].

$app is the container and the $name is the facade accessor from earlier. Now comes the singleton part when Laravel adds the returned value to a static array called $resolvedInstance, and if we look at the code before this, we will see that Laravel checks if the instance is already present in that array.

1protected static function resolveFacadeInstance($name)
2{
3    if (is_object($name)) {
4        return $name;
5    }
6 
7    if (isset(static::$resolvedInstance[$name])) {
8        return static::$resolvedInstance[$name];
9    }
10 
11    if (static::$app) {
12        return static::$resolvedInstance[$name] = static::$app[$name];
13    }
14}

Since this array is static, it will remember it's content throughout the entire request. That means that if we have resolved a facade once, then the next time we resolve it we get the same instance! 🤯

How to work around this!

Laravel itself is actually quite good at working around this. Let's take the example of the Http client. If we did this...

1$response1 = Http::withHeaders([
2    'X-First' => 'foo',
3    'X-Second' => 'bar'
4])->post('http://example.com/users', [
5    'name' => 'Taylor',
6]);
7 
8$response2 = Http::get('http://example2.com/users');

...it would be easy to assume that we would be using the same Http client instance, and yes, in fact we are! However laravel does a clever trick here with the methods. These withheaders(), post(), get(), etc. don't actually exist on the underlying class. The class that the Http facade refers to is a Factory class which has the following method instead.

1public function __call($method, $parameters)
2{
3    if (static::hasMacro($method)) {
4        return $this->macroCall($method, $parameters);
5    }
6 
7    return tap($this->newPendingRequest(), function ($request) {
8        $request->stub($this->stubCallbacks);
9    })->{$method}(...$parameters);
10}

So when you call a method that does not exist like get() it will first check if there is a macro with that name. If not it will create a new PendingRequest class instance, and return that.

1protected function newPendingRequest()
2{
3    return new PendingRequest($this);
4}

An easier fix

If you need to ensure that you always get a new instance when using the facade, you can do this little trick (inspired by Spatie) on the class you are resolving by adding this.

1public function new()
2{
3    return new static();
4}

You can then use the facade like this, to always get a new instance

1$myclass = MyAwesomeClassFacade::new();
2$myclass->setName('foobar');

This will call the new() method which will ensure that you get a new instance.

Note: Spatie has taken the facade completely out of the equation, and simply allow using the class itself as if it was a facade.

1public static function new()
2{
3    return new static();
4}

Notice the static keyword. That means we can run it one the class directly.

1$myclass = MyAwesomeClass::new();
2$myclass->setName('foobar');

Wrapping up

I hope this was helpful and that you learned something new. This post was inspired by: Laravel GitHub issue #1088

]]>
2021-12-16T14:00:20+00:00