Category Archives for programming

Change field to enum in Laravel 10

In this post I will show you how to change an existing field in your Laravel application to change to enum.

I have a field status which was kept in line by having class constants defining the value for this field. But I really missed the database keeping the value in line though.

Changing the class constants to a PHP 8 enum and changing the database field to enum had some caveats so if you want to change your datatype to enum as well, follow along.

Change the class constants to an enum

I use the trait EnumEnhancements for the enum as this provides very handy helper methods:

ImportStatus::valueArray(): an array with the values of the enum
ImportStatus::valueList(): a string with the values of the enum: ‘pending, validating, importing, …’

<?php

declare(strict_types=1);

namespace App\Models\Enums;

use Othyn\PhpEnumEnhancements\Traits\EnumEnhancements;

enum ImportStatus: string
{
    use EnumEnhancements;

    case PENDING = 'pending';
    case VALIDATING = 'validating';
    case VALIDATION_FAILED = 'validation_failed';
    case VALIDATED = 'validated';
    case CREATING_IMPORT_JOBS = 'creating_import_jobs';
    case IMPORT_JOBS_CREATED = 'import_jobs_created';
    case IMPORTING = 'importing';
    case IMPORTED = 'imported';
    case FAILED = 'failed';
    case CANCELLED = 'cancelled';
    case FINISHED = 'finished';
}

Create a Laravel migration to change the field to an enum

NOTE: changing an existing field to an enum in your database requires you to do this with a DB statement. You cannot use a query builder for this or you’ll get the following error :

Unknown column type "enum" requested. Any Doctrine type that you use has to be registered with \Doctrine\DBAL\Types\Type::addType(). You can get a list of all the known types with \Doctrine\DBAL\Types\Type::getTypesMap(). If this error occurs during database introspection then you might have forgotten to register all database types for a Doctrine Type. Use AbstractPlatform#registerDoctrineTypeMapping() or have your custom types implement Type#getMappedDatabaseTypes(). If the type name is empty you might have a problem with the cache or forgot some mapping information.

I change the existing column from a string to an enum in 4 steps:

  1. Get the current status of each record so it won’t get lost in the following steps
  2. Set all current values to NULL so that changing the datatype of the field won’t cause trouble with existing values
  3. Do the actual changing of the datatype of the field to enum
  4. Change all statuses back to their original value but act on cases where the existing value is not valid with the current enum values

<?php

declare(strict_types=1);

use App\Models\Enums\ImportStatus;
use App\Models\ImportFile;
use Illuminate\Database\Migrations\Migration;

return new class () extends Migration {
    public function up(): void
    {
        // 1. Get the current status for each row
        $importFiles = DB::table('posts')->get(['id', 'status']);

        // 2. Set all statuses to NULL
        DB::table('posts')->update(['status' => null]);

        // 3. Change the column to an enum
        $this->changeStatusToEnum();

        // 4. Set the status for each row to the value we got in step 1
        foreach ($products as $product) {
            $validStatuses = collect(ImportStatus::valueArray());
            $currentStatus = strtolower($product->status ?? '');
            $currentStatus = $currentStatus === 'success' 
                ? ImportStatus::FINISHED->value  
                : $currentStatus;
            $newStatus = $validStatuses->contains($currentStatus)
                ? $currentStatus
                : ImportStatus::PENDING->value;

            // update the status, if one fails set the status to 
            // the default value
            Post::where('id', $post->id)
                ->update(['status' => $newStatus]);
        }
    }

    private function changeStatusToEnum(): void
    {
        // note: enums cannot be changed with migrations. 
        //       It has to be done with a DB statement
        $validEnumValues = ImportStatus::valueList("', '");
        $defaultValue = ImportStatus::PENDING->value;
        $query = "ALTER TABLE posts";
        $query .= "MODIFY COLUMN status enum('{$validEnumValues}') ";
        $query .= "DEFAULT '{$defaultValue}'";
        DB::statement($query);
    }
};

Update current references to the class constants to the enum value

A little drawback is that I can no longer refer to the values as ImportStatus::validating as that is now returning an enum instead of a string.

To get the value of an enum you have to call the value property on it: ImportStatus::validating->value

So change your codebase by replacing ImportStatus::validating to ImportStatus::validating->value

Laravel order records by days until date – like the next birthday

You might want to order your users by ‘days until their birthday’.

People having their birthday come first, people with no birthday registered (NULL values), come last.

// in User model
public static function getByBirthday()
{
  return User::query()
      ->select('user.*')
      ->selectRaw(
          '365.25 -
          (
            case
              WHEN TIMESTAMPDIFF(day, birthday, CURDATE()) = 0 THEN 364.25
              WHEN birthday IS NULL THEN 0
              ELSE TIMESTAMPDIFF(day, birthday, CURDATE())
            end
            mod 365.25
           ) AS days_till_birthday'
      )
      ->orderBy('days_till_birthday');
}

// use it in your code like
$usersByBirthday = User::getByBirthday()->get();

Laravel API returns 401 even while logging in succeeds

For me, it turned out the stateful property in config/sanctum.php was not filled correctly.

After setting it to the default as shown below, it started working.

// file config/sanctum.php
...
    'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
        '%s%s',
        'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
        Sanctum::currentApplicationUrlWithPort()
    ))),
...

Did this not fix your problem?

Check this post which might help: https://stackoverflow.com/a/69858100

Generate random values in Postman to use in your tests

When you want your tests to be able to run whenever you want, you should use values which are random.

In Postman, click on the name of Collection and then open the ‘Pre-request Script’ tab.

There, add the following:

// get a random number between a minimum and a maximum
// gives you current datetime with milliseconds like 2022810_171012_174
postman.setGlobalVariable("getCurrentDate",  () => {
  const date=new Date(); 
  return String(date.getFullYear())  
      + String(date.getMonth()+1) 
      + String(date.getDate()) 
      + '_' 
      + String(date.getHours() < 10 ? "0"+date.getHours() : date.getHours()) 
      + String(date.getMinutes() < 10 ? "0"+date.getMinutes() : date.getMinutes()) 
      + String(date.getSeconds() < 10 ? "0" + date.getSeconds() : date.getSeconds())
      + '_' 
      + String(date.getMilliseconds())
})

You can now use this function in your tests. This enables you to make your strings (like emailaddresses) random by adding the current datetime to it.

To use it, open your test, click on the ‘Pre-request Script’ tab and add the following.

var currentDate = eval(pm.globals.get("getCurrentDate"))();
var randomEmail = `postman-${currentDate}@pauledenburg.com`;
pm.environment.set("randomEmail", randomEmail);

You can now use the generated value in the body of your POST-request by referencing it as {{randomEmail}}

Gulp error: Did you forget to signal async completion?

First of all: do check the Gulp documentation on this: https://gulpjs.com/docs/en/getting-started/async-completion#using-async-await

I had the following gulpfile.js:

# file gulpfile.js
function build() {
    return series(
        clean,
        parallel(
            images,
            tracker,
            fonts
        ),
        clean_busters
    );
}

exports.build = build;

When running gulp build I got the following errors:

$ gulp build
[11:17:33] Using gulpfile ./gulpfile.js
[11:17:46] The following tasks did not complete: build
[11:17:46] Did you forget to signal async completion?

Solution

I fixed it by making the build() function async: async build().
Then my gulpfile.js looked like the following (note the extra parentheses at the end!)

# file gulpfile.js
async function build() {
    return series(
        clean,
        parallel(
            images,
            tracker,
            fonts
        ),
        clean_busters
    )();
}

exports.build = build;

Slim 2 framework logging

This shows you how to enable logging so you can write stuff like $app->log->debug('this will show up in the error_log');.

<?php

--- snip %< ---

$app = new \Slim\Slim(array(
    'log.enabled' => true,
    'log.level'   => \Slim\Log::DEBUG
));

$app->log->debug('this will show up in your error-log');

--- >% /snip ---

 

[opt-in]

404 Not found on SSL certificate renew with certbot from letsencrypt

I spend a lot of time figuring out why I kept getting a ‘404 Not Found’ when I wanted to renew my SSL Certificate with certbot.

Long story short: invalid ipv6 DNS Mapping.

I got it working by removing the ipv6 DNS entry. I’ll be fixing it in a proper way when there is more time available.

But there were other gotcha’s as well:

  • basic auth on the directory
  • iptables blocking certain traffic

 

[opt-in]

Gitlab CI upload artifact fails: too large

Today I wanted to add a package-job to my Gitlab CI as instructed in this nice Gitlab tutorial.

I created the tar-file but when it came to uploading it failed with Request entity too large.

(...)
ERROR: Uploading artifacts to coordinator... too large archive  id=243 responseStatus=413 Request Entity Too Large status=413 Request Entity Too Large token=JYszbA9F
FATAL: Too large                                   
ERROR: Job failed: exit status 1

It took me some digging, but this is how I fixed this (note, the Nginx proxy was the one giving me a hard time).

Step 1: Set the maximum artifacts size

In your gitlab, go to Settings > Continuous Integration and Deployment > Maximum artifacts size (MB) and set it to the desired value. The default is 100MB.

Step 2: Set the nginx upload size

In the gitlab.rb file, mine at /etc/gitlab/gitlab.rb, set or uncomment the following line.

nginx['client_max_body_size'] = '250m'

 

 

And reconfigure gitlab to get this to work.

gitlab-ctl reconfigure

Step 3: (optional) update your proxy(!)

I run gitlab on docker containers. On the server, I run nginx as a proxy to redirect requests for gitlab to these containers.

I failed to update the proxy configuration to allow the POST-ing of data to the containers.

As I use nginx, this is the line I added. For Apache, just google and you’ll find your answer.

client_max_body_size 0;

This will set no limits on clients sending data.

For reference, this is my whole nginx vhost file.

server {
    listen 80;
    server_name git.pauledenburg.com;
    client_max_body_size 0;

    location / {
        proxy_pass http://localhost:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Don’t forget to reload nginx.

$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

$ sudo service nginx reload