Docker helper function to get a shell

Want to get your bash shell in a running docker container?

run: deit application
(deit, as in: ‘Docker Exec -IT’)

and will show you all running containers which have ‘application’ in the name.

Add the following function to your ~/.bashrc or ~/.zshrc

# Function to display matching containers and allow user selection
function deit {
    container_query="$1"
    shift 1
    container_command="${@:-bash}"  # Default to 'bash' if no command is provided

    # Get the list of matching container names
    matching_containers=($(docker ps --format '{{.Names}}' | grep "$container_query"))
    count=${#matching_containers[@]}

    if [ "$count" -eq 0 ]; then
        echo "No containers found with '$container_query' in the name."
        return 1
    elif [ "$count" -eq 1 ]; then
        container_name="${matching_containers[0]}"
        echo "Found container: $container_name"
        echo "Executing: docker exec -it $container_name $container_command"
        docker exec -it "$container_name" $container_command
    else
        echo "Multiple containers found with '$container_query' in the name:"
        for i in "${!matching_containers[@]}"; do
            echo "$((i + 1)). ${matching_containers[i]}"
        done

        while true; do
            read -p "Choose the number of the container you want to use: " choice
            if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le "$count" ]; then
                container_name="${matching_containers[$((choice - 1))]}"
                echo "Selected container: $container_name"
                echo "Executing: docker exec -it $container_name $container_command"
                docker exec -it "$container_name" $container_command
                break
            else
                echo "Invalid choice. Please try again."
            fi
        done
    fi
}

Install multiple versions of PHP on Ubuntu 24.04

Works on other versions of Ubuntu as well.

Let’s install PHP8.1, PHP8.2 and PHP8.3 on our Ubuntu machine.

Add the following script as ~/install-multiple-php-versions.sh.

#!/bin/bash

# file: ~/install-multiple-php-versions.sh

# Check if the Ondřej Surý PPA repository is already added
if ! grep -q "ondrej/php" /etc/apt/sources.list /etc/apt/sources.list.d/*; then
    echo "Adding Ondřej Surý PPA repository to get the latest PHP versions..."
    sudo add-apt-repository -y ppa:ondrej/php
    sudo apt update
fi

# Function to install a PHP version
install_php_version() {
    local version="$1"
    sudo apt install -y "php$version" "php$version-cli" "php$version-fpm"  "php$version-common" "php$version-zip" "php$version-mbstring" "php$version-opcache"
}

# Start installation message
echo "Starting installation of PHP versions..."

# Install different PHP versions
versions=("8.0" "8.1" "8.2" "8.3")
for version in "${versions[@]}"; do
    install_php_version "$version"
    echo "PHP $version installed successfully."
done

# Completion message
echo "All PHP versions have been installed successfully."

# Set default PHP version (optional)
# sudo update-alternatives --set php /usr/bin/phpX.X  # Replace X.X with desired version

# Check installed PHP versions
php -v

# Check all installed PHP versions
dpkg -l | grep php | awk '{print $2}' | grep -E "^php[0-9]+\.[0-9]+$"

Allow it to execute and then execute:

chmod +x ~/install-multiple-php-versions.sh
. ~/install-multiple-php-versions.sh

Add the script to switch php versions over at /usr/local/bin/sphp

#!/bin/bash

# file: /usr/local/bin/sphp
# switch PHP versions with: sphp 8.3

# Function to check if a command exists
command_exists() {
    type "$1" &> /dev/null
}

# Function to show the current PHP version
show_current_php() {
    php --version | head -n 1
}

# Main function to change PHP version for the current user
switch_php() {
    if [ -z "$1" ]; then
        echo "Usage: sphp <version>"
        echo "Example: sphp 8.1"
        exit 1
    fi

    VERSION=$1
    PHP_BIN="php$VERSION"

    if ! command_exists $PHP_BIN; then
        echo "PHP $VERSION is not installed. Install it first using apt."
        exit 1
    fi

    # Check if update-alternatives --set-user is supported
    if update-alternatives --help | grep -q "\-\-set-user"; then
        # Use update-alternatives --set-user if it's supported
        update-alternatives --set-user php /usr/bin/$PHP_BIN
    else
        # Alternative method using symlinks if --set-user is not available
        mkdir -p ~/.local/bin
        ln -sf /usr/bin/$PHP_BIN ~/.local/bin/php

        # Add ~/.local/bin to PATH if it's not already there
        if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then
            echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
            export PATH="$HOME/.local/bin:$PATH"
        fi
    fi

    echo "Switched to PHP $VERSION for user $USER"
    show_current_php
    echo "Note: You may need to open a new terminal or run 'source ~/.bashrc' to see the change."
}

# Execute the main function
switch_php "$1"

And make it executable

sudo chmod +x /usr/local/bin/sphp

How to set up your Mac mini to reboot without a monitor and keyboard

If you’re using your Mac mini as a headless server, you might have found that you cannot login using ssh or Screen Sharing after a reboot.

To overcome this, only reboot using the command sudo fdesetup authrestart

This will prompt you for the user to login with after reboot, but it won’t log in to the desktop.
It just shows the login screen on the Mac mini, but now you are able to remotely login to the Mac mini after a reboot without a monitor or keyboard attached.

# to reboot and have remote services accessible:
sudo fdesetup authrestart

# tip: make an alias of 'reboot'
echo 'sudo fdesetup authrestart' >> ~/.bash_profile

# after re-login, you can now just type: reboot

Launch shell in Docker container via command line function

Add the following function somewhere where it will be picked up by your shell.

For example: ~/.bash_profile

Then use it like so:

$ deit api
Multiple containers found with 'api' in the name:
1. srv-captain--strippenkaart-api.1.ytwsvciqerfm3k0821tpowx23
2. srv-captain--verkooprapport-api.1.we0g3exhke86q2k2qgikzkmyk
3. srv-captain--trainingsomgeving-api.1.624q0sm2qkpbqxyn37osiq2er
4. srv-captain--funnelmonitor-api.1.phukkl9slxifjcvg3yx2ykdda
Choose the number of the container you want to use:

This is the bash function:

# in file ~/.bash_profile
# 'deit' as in: docker exec -it 
function deit {
    container_query="$1"
    shift 1
    container_command="${@:-bash}"  # Default to 'bash' if no command is provided

    # Get the list of matching container names
    matching_containers=($(docker ps --format '{{.Names}}' | grep "$container_query"))
    count=${#matching_containers[@]}

    if [ "$count" -eq 0 ]; then
        echo "No containers found with '$container_query' in the name."
        return 1
    elif [ "$count" -eq 1 ]; then
        container_name="${matching_containers[0]}"
        echo "Found container: $container_name"
        echo "Executing: docker exec -it $container_name $container_command"
        docker exec -it "$container_name" $container_command
    else
        echo "Multiple containers found with '$container_query' in the name:"
        for i in "${!matching_containers[@]}"; do
            echo "$((i + 1)). ${matching_containers[i]}"
        done

        while true; do
            read -p "Choose the number of the container you want to use: " choice
            if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le "$count" ]; then
                container_name="${matching_containers[$((choice - 1))]}"
                echo "Selected container: $container_name"
                echo "Executing: docker exec -it $container_name $container_command"
                docker exec -it "$container_name" $container_command
                break
            else
                echo "Invalid choice. Please try again."
            fi
        done
    fi
}

npm install: ERR_SSL_CIPHER_OPERATION_FAILED and ERR_SSL_CIPHER_OPERATION_FAILED

I got these errors after trying to run npm install.

# npm install

...
npm ERR! gyp ERR! UNCAUGHT EXCEPTION
npm ERR! gyp ERR! stack TypeError: Cannot assign to read only property 'cflags' of object '#<Object>'
npm ERR! gyp ERR! stack     at createConfigFile (/app/node_modules/node-gyp/lib/configure.js:117:21)
npm ERR! gyp ERR! stack     at /app/node_modules/node-gyp/lib/configure.js:84:9
npm ERR! gyp ERR! stack     at FSReqCallback.oncomplete (node:fs:191:23)
npm ERR! gyp ERR! System Linux 6.6.12-linuxkit
npm ERR! gyp ERR! command "/usr/local/bin/node" "/app/node_modules/node-gyp/bin/node-gyp.js" "rebuild" "--verbose" "--libsass_ext=" "--libsass_cflags=" "--libsass_ldflags=" "--libsass_library="
npm ERR! gyp ERR! cwd /app/node_modules/node-sass
npm ERR! gyp ERR! node -v v20.11.1
npm ERR! gyp ERR! node-gyp -v v7.1.2
npm ERR! gyp ERR! Node-gyp failed to build your package.
npm ERR! gyp ERR! Try to update npm and/or node-gyp and if it does not help file an issue with the package author.
npm ERR! Build failed with error code: 7

I did the following to fix these

  • installed system packages
  • updated node to the current version (not LTS)
  • update npm to the latest version
  • update node_gyp to the latest version
  • updated all versions in package.json to the latest versions
  • removed package-lock.json file
  • removed node_modules directory
  • cleaned npm cache
  • ran npm install again

I installed the following packages:

# apt-get update && apt-get install -y ca-certificates curl gnupg zip unzip git software-properties-common rsyslog

Upgraded node, npm, node_gyp and cleared cache

 # npm cache clean -f \
  && npm install -g n \
  && n current
# npm install -g npm@latest
# npm update -g node-gyp
# rm -rf node_modules package-lock.json

Then, update all package versions in package.json to their latest version.

And then tried again, this time it worked.

# npm install

Did this work for you as well? Had to do less? More? Let me know and I’ll update this post.

Add pint to PHPStorm File Watchers

Open your settings in PHPStorm and go to Tools > File Watchers

Add a new Custom File Watcher and use the following information:

Name: pint
File type: PHP
Program: $ProjectFileDir$/vendor/bin/pint
Arguments: $FileRelativePath$
Output paths to refresh: $FileRelativePath$
Working directory: $ProjectFileDir$

Add url to your laravel Exception context

props to @_newtonjob https://twitter.com/_newtonjob/status/1724023158787694864/photo/1

<?php
// file app/Exceptions/Handler.php
class Handler extends ExceptionHandler
{
   
    public function register(): void
    {
       
       //
    }

    public function context()
    {
        return array_merge(parent::context(), [
            'url' => url()->full(),
            'method' => request()->getMethod(),
        ]);
    }
}

Show your query with values in Laravel 10 Eloquent

When debugging your eloquent model queries, it’s often nice to know which SQL query is executed in the end.

By default, Laravel won’t show you the values used in the query. Instead, it shows you a questionmark ? instead of the actual values. This is because of the parameter bindings in PDO.

Let’s create a ->toSqlWithBindings() macro so you can have the full query.

Create the macro as ServiceProvider

<?php

// file app/Providers/MacroServiceProvder.php

declare(strict_types=1);

namespace App\Providers;

use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;

class MacroServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Builder::macro('toSqlWithBindings', function () {
            /** @var Builder $this */
            $bindings = array_map(
                fn ($parameter) => (is_string($parameter) || $parameter instanceof Carbon) ? "'{$parameter}'" : $parameter,
                /** @var Builder $this */
                $this->getBindings()
            );

            return Str::replaceArray(
                '?',
                $bindings,
                $this->toSql()
            );
        });

        /** @var Builder $this */
        EloquentBuilder::macro('toSqlWithBindings', fn () => $this->toBase()->toSqlWithBindings());
    }
}


the serviceprovider file containing the macro which gives us the toSqlWithBindings() method on eloquent queries

Use it

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 migration change foreign id to be nullable

If you created a foreign key in your database which should have been nullable, you have to create a database migration to fix that.

To make your foreign key nullable, you first need to delete the Foreign Key constraint, then change the field to be nullable and then create the Foreign Key constraint again.

<?php

return new class extends Migration {
    public function up(): void
    {
        Schema::table('retailers', function (Blueprint $table) {
            $table->dropForeign('retailers_import_id_foreign');

            $table->unsignedBigInteger('import_id')
                ->nullable(true)
                ->change();

            $table->foreign('import_id')
                ->references('id')
                ->on('import_files')
                ->nullOnDelete()
                ->cascadeOnUpdate();
        });
    }
};
1 2 3 10