Inventory app — saving inventory items.

This is the absolute bare bones minimum implementation for my inventory keeping: saving items to my inventory list.

Super simple, but meant only as an example of how I’d work when working on an API.

Here are the changes made to my Inventory Manager. Those changes include the test and logic for the initial index endpoint too. I may blog about that part in a separate post soon.

Writing the store test

One of Laravel’s many strengths is how well it is set up for testing and just how nice those tests can read. Especially now that I’ve started using Pest.

Here is the test I wrote for the store endpoint I was yet to write:

test('inventory items can be created', function () {
    $response = $this->postJson(route(name: 'inventory.store'), [
        'name' => 'My Special Item',
    ]);

    $response->assertStatus(201);

    $this->assertDatabaseHas(Inventory::class, [
        'name' => 'My Special Item',
    ]);
});

Firstly I post to an endpoint, that I am yet to create, with the most minimal payload I want: an item’s name:

$response = $this->postJson(route(name: 'inventory.store'), [
    'name' => 'My Special Item',
]);

Then I can check I have the correct status code: an HTTP Created 201 status:

$response->assertStatus(201);

Finally I check that the database table where I will be saving my inventory items has the item I have created in the test:

$this->assertDatabaseHas(Inventory::class, [
    'name' => 'My Special Item',
]);

The first argument to the assertDatabaseHas method is the model class, which Laravel will use to determine the name of the table for that model. Either by convention, or by the value you override it with on the model.

The second argument is an array that should match the table’s column name and value. Your model can have other columns and still pass. It will only validate that the keys and values you pass to it are correct; you don’t need to pass every column and value — that would become tedious.

Writing the store logic

There is a term I’ve heard in Test-driven development called “sliming it out”. If I remember correctly, this is when you let the test feedback errors dictate every single piece of code you add.

You wouldn’t add any code at all until a test basically told you too.

I wont lie – I actually love this idea, but it soon becomes tiresome. It’s great to do when you start out in TDD, in my opinion, but soon you’ll start seeing things you can add before running the test.

For example, you know you’ll need a database table and a model class, and most likely a Model Factory for upcoming tests, so you could run the artisan command to generate those straight away:

php artisan make:model -mf Inventory

# or with sail
./vendor/bin/sail artisan make:model -mf Inventory

I dont tend to generate my Controller classes with these, as I now use single-action controllers for personal projects.

Store Route

Within the routes/web.php file, I add the following:

use App\Http\Controllers\Inventory\StoreController;

Route::post('inventory', StoreController::class)->name('inventory.store');

Using a single-action class here to keep logic separated. Some would see this as over-engineering, especially if keeping controller code to a minimum anyway, but I like the separation.

Adding an explicit “name” to the endpoint, means I can just refer to it throughout the app with that name. Like in the test code above where I generate the endpoint with the “route” helper function:

route(name: 'inventory.store')

Store Controller

<?php

declare(strict_types = 1);

namespace App\Http\Controllers\Inventory;

use App\Http\Requests\InventoryStoreRequest;
use App\Models\Inventory;
use Illuminate\Contracts\Routing\ResponseFactory;
use Illuminate\Http\Response;

class StoreController
{
    public function __invoke(InventoryStoreRequest $request): Response|ResponseFactory
    {
        Inventory::create([
            'name' => $request->get(key: 'name'),
        ]);

        return response(content: 'Inventory item created', status: 201);
    }
}

Super straight forward at the moment. After receiving the request via the custom request class (code below), I just create an inventory item with the name on the request.

I then return a response with a message and an HTTP Created 201 status.

This code does assume that it was created fine so I might look at a better implementation of this down the line…

…but not before I have a test telling me it needs to change.

InventoryStoreRequest class

This is a standard generated request class with the following rules method:

/**
 * Get the validation rules that apply to the request.
 *
 * @return array<string, mixed>
 */
public function rules(): array
{
    return [
        'name' => 'required',
    ];
}

Again, nothing much to it. It makes sure that a name is required to be passed.

Its not saying anything about what that value could be. We could pass a date time or a mentally-long string.

I’ll fix that in a future post.

An extra test for the required name

In order to be “belt and braces”, I have also added a test that proves that we require a name to be passed. Pest makes this laughable simple:

test('inventory items require a name', function () {
    $this->postJson(route(name: 'inventory.store'))
        ->assertJsonValidationErrorFor('name');
});

This just performs a post request to the store endpoint, but passes no data. We then just chain the assertJsonValidationErrorFor method, giving it the parameter that should have caused the failed validation. In this case “name”.

As the validation becomes more sophisticated I will look at adding more of these tests, and even possibly running all “required” fields through the some test method with Pests data functionality. Essentially the same as how PHPUnit’s Data Providers work.

Useful Links

Complete changes in git for when I added the store and the index endpoints to my Inventory app.

PHP Psalm warning for RouteServiceProvider configureRateLimiting method

When running psalm in a Laravel project, I get the following error by default:

PossiblyNullArgument - app/Providers/RouteServiceProvider.php:45:46 - 
Argument 1 of Illuminate\Cache\RateLimiting\Limit::by cannot be null, 
possibly null value provided

This is the default implementation for configureRateLimiting in the RouteServiceProvider class in Laravel:

protected function configureRateLimiting()
{
    RateLimiter::for('api', function (Request $request) {
        return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
    });
}

I change it to the following to get psalm to pass (I’ve added named parameters and the static keyword before the callback function):

protected function configureRateLimiting()
{
    RateLimiter::for(name: 'api', callback: static function (Request $request) {
        $limitIdentifier = $request->user()?->id ?: $request->ip();
        if (!is_null($limitIdentifier)) {
            return Limit::perMinute(maxAttempts: 60)->by(key: $limitIdentifier);
        }
    });
}

Sprinklings of Docker for local development

When I search for docker-related topics online, it almost seems to me that there are two trains of thought for the most part:

  • Those who use a full docker / docker-compose setup for local development.
  • Those who hate and/or fear docker and would rather just install and do everything locally.

I believe either of these is a valid approach — whatever feels right to you. Of course it does also depend on how your company / team works.

But I’d like to introduce you to a third way of working on a project — sprinklings of docker, I call it 😀.

The idea is essentially to just use docker for certain things in a project as you develop it locally.

This is how I tend to work, but is by no means what I would call “the right way”; it’s just what works best for me.

How I work with Docker.

I am primarily a Laravel developer, and work as such at the excellent company — and Laravel PartnerJump 24.

As I am a php developer, it stands to reason that I have php installed on my system. I also have nginx installed, so I can run a php application locally and serve it at a local domain without needing docker.

Historically, when I would need a MySQL database (which is often the case) I would have gotten MySQL installed on my system.

Which is fine.

But I’m becoming a bit of a neat freak in my older age and so want to keep my computer as clean as possible within reason.

So what I do now is start a new docker container for MySQL and connect to that instead:

# Bash command to start up a docker container with MySQL in it
# And use port 33061 on my local machine to connect to it.
docker run \
--name=mysql \
--publish 33061:3306 \
--env MYSQL_DATABASE=my_disposable_db \
--env MYSQL_ROOT_PASSWORD=password \
--detach mysql

Then in my Laravel .env configuration I would add this:

DB_HOST=0.0.0.0:33061
DB_DATABASE=my_disposable_db
DB_USERNAME=root
DB_PASSWORD=password

The benefit of working this way is that if anything happens to my MySQL container — any corruptions or just ending up with a whole mess of databases old and new in there, I can just destroy the container and start a new one afresh.

Not to mention when I want to upgrade the MySQL version im working with… or even test with a lower version.

docker container stop mysql
docker container rm mysql
# And then re-run the "docker run" command above.
# Or even run it with different variables / ports.

The same goes for any other database engines too: Postgres; Redis; MariaDB. Any can just be started up on your system as a standalone Docker container and connected to easily from your website / app in development.

# Start a Postgres container
docker run \
--name postgres \
--publish 5480:5432 \
--env POSTGRES_PASSWORD=password \
--detach postgres:11-alpine

# Start a redis container
docker run \
--name redis \
--publish 6379:6379 \
--detach redis

# Start a Mariadb container
docker run \
--name some-mariadb \
--publish 33062:3306 \
--env MARIADB_USER=example-user \
--env MARIADB_PASSWORD=my_cool_secret \
--env MARIADB_ROOT_PASSWORD=my-secret-pw  \
--detach mariadb

And with them all being self-contained and able to be exposed to any port on the host machine, you could have as many as you wanted running at the same time… if you were so inclined.

I love how this approach keeps my computer clean of extra programs. And how it makes it super easy to have multiple versions of the same thing installed at the same time.

Docker doesn’t have to be scary when taken in small doses. 😊