Joseph Edmonds

Joseph's Blog

Running Node with Docker

I recently decided to set up my local system without node installed and instead user Docker so that I can very easily flip versions and don’t have any complications about incompatabilies etc.

To install Docker, I follow the normal Fedora instructions and I also set up the rootless mode, so that containers run as me rather than root.

Then once Docker is installed, I add this to my bashrc file and then basic node commands run inside a container.


# Docker Node stuff
DOCKER_NODE_VER=${DOCKER_NODE_VER:-16}
docker-node-version() {
case "$1" in
-s|--set)
if [ "$2" ]; then
DOCKER_NODE_VER="$2"
echo
echo "docker-node will now use node:$2 image!"
fi
return 0
esac
echo $DOCKER_NODE_VER
}
docker-node-image() {
echo -n "node:`docker-node-version`"
}
docker-node-run() {
set -x
local dp
# [ "$1" = "bash" ] && dp="-it"
dp="$dp -it --init --rm"
dp="$dp -p 8080:8080"
dp="$dp -v "$PWD":/usr/src/app"
dp="$dp -w /usr/src/app"
docker run $dp $(docker-node-image) "$@"
set +x
}
node() { docker-node-run "$@"; }
npm() { docker-node-run npm "$@"; }
npx() { docker-node-run npx "$@"; }
yarn() { docker-node-run yarn "$@"; }



This then means that any call to node, npm, npx or yarn will actually run in a container. I can easily change the node version by either directly overriding the DOCKER_NODE_VER variable, or calling the docker-node-version function.

Note – I didn’t create this from scratch, the original is here:
https://github.com/paulojeronimo/docker-node-shell-functions

PHP Exceptions: Writing for clarity and testability

Personally I have chosen this style as it makes my life easy and I find it clear and easy to work with.

Take a look at this PHPUnit test:

https://github.com/LongTermSupport/microdeps-curl/blob/master/tests/CurlHandleFactoryTest.php#L70-L77

    /**
     * @test
     * @covers \MicroDeps\Curl\CurlException
     */
    public function itExceptsOnInvalidLogFolder(): void
    {
        $this->expectException(CurlException::class);
        $this->expectExceptionMessage(
            sprintf(
                CurlException::MSG_DIRECTORY_NOT_CREATED,
                '/invalid/path',
                'mkdir(): Permission denied'
            )
        );

The test is going to do something that is expected to throw an exception of a specific type and the exception message should also be an explicit string.

PHPUnit has excellent support for testing exceptions as documented here:

https://phpunit.readthedocs.io/en/9.5/writing-tests-for-phpunit.html#testing-exceptions

Keeping it DRY

Now, hopefully you are already familiar with the concept of DRY when coding. DRY stands for “Don’t Repeat Yourself” and the principle is quite simple – you should not be copying and pasting stuff around. Instead there should be a single source for things that you can then include in your code base in multiple places are required.

In this code, we are using a constant to define the exception message and that means that we can easily change the actual wording of the exception message, without breaking this test, nor having to do any bulk find/replace type shenanigans.

The exception message is stored as a public constant on the exception class that it is related to:

https://github.com/LongTermSupport/microdeps-curl/blob/master/src/CurlException.php#L14

public const MSG_DIRECTORY_NOT_CREATED      = 'Directory "%s" was not created, error: %s';

By storing the exception message as a constant, we make writing tests a lot easier and can be very precise about the expected exception message.

Message Format

The exception message is a special kind of string that implements a “format” which can then be used with the sprintf built in function. The format has placeholders for values that are then passed in when calling the function.

When we want to throw the exception, we need to also use the same constant and pass in the required parameters. For example:

https://github.com/LongTermSupport/microdeps-curl/blob/master/src/CurlHandleFactory.php#L76-L83

           if (!is_dir($logFileDir) && !@mkdir($logFileDir, 0755, true) && !is_dir($logFileDir)) {
                $error = error_get_last();
                throw CurlException::withFormat(
                    CurlException::MSG_DIRECTORY_NOT_CREATED,
                    $logFileDir,
                    $error['message'] ?? 'unknown error'
                );
            }

Static Exception Create Method

You’ll notice that the throw statement does not use the common style of throw new CurlException but instead is calling a static method on the CurlException class which acts as a factory method to create an instance of the exception with a specific message.

Here is that method:

https://github.com/LongTermSupport/microdeps-curl/blob/86bb3e41323b7d6521d9f6b23dd5d32b70ee8a76/src/CurlException.php#L27-L30

    public static function withFormat(string $format, string ...$values): self
    {
        return new self(sprintf($format, ...$values));
    }

Previous Exceptions

Where the Exception is being thrown after catching a previous Exception, it’s important that we log that correctly. For that reason there is a separate withFormatAndPrevious method:

https://github.com/LongTermSupport/microdeps-curl/blob/86bb3e41323b7d6521d9f6b23dd5d32b70ee8a76/src/CurlException.php#L32-L35

public static function withFormatAndPrevious(string $format, Throwable $previous, string ...$values): self
    {
        return new self(sprintf($format, ...$values), $previous);
    }

This is used when catching another Exception but wanting to ensure that our code emits a standard Exception type so that it can be handled easily, for example:

https://github.com/LongTermSupport/microdeps-curl/blob/86bb3e41323b7d6521d9f6b23dd5d32b70ee8a76/src/CurlConfigAwareHandle.php#L46-L59

     try {
            curl_setopt_array($this->handle, $this->options->get());
        } catch (ValueError $valueError) {
            $valid   = array_flip(get_defined_constants(true)['curl']);
            $invalid = array_diff_key($this->options->get(), $valid);
            throw CurlException::withFormatAndPrevious(
                CurlException::MSG_INVALID_OPTIONS,
                $valueError,
                print_r($invalid, true),
                /* @phpstan-ignore-next-line confused by curl_version return type */
                curl_version()['version']
            );
        }

Private Constructor

To enforce that we always throw our exceptions with a format, I’ve actually made the constructor for the Exception class private.

https://github.com/LongTermSupport/microdeps-curl/blob/86bb3e41323b7d6521d9f6b23dd5d32b70ee8a76/src/CurlException.php#L21-L25

    private function __construct(string $message, Throwable $previous = null)
    {
        $message = $this->appendInfo($message);
        parent::__construct($message, 0, $previous);
    }

Conclusion

This is just my style. I’m not saying its the only way to do things, but I do think it’s nice and worth considering.

This approach to structuring your Exception classes and predefining the exception message as constants within the Exception class is, I believe, a nice tidy way of managing your exception messages and also making them easy to test.

This approach allows you to have “general” Exception classes that can be used in multiple error scenarios without feeling the need to create a specific Exception class for each and every error scenario.

Most importantly, it allows you to easily and accurately test your exception throwing code to confirm the exact message.

Simple Nginx Trick to Reduce Server Load

It is quite common to have some Nginx config along the lines of

location / {
    try_files $uri $uri/ /index.php$is_args$args;
}

What this does is tries the exact URL, tries the URL as a folder and if not, passes the request to PHP to be processed by website system, eg Magento/Symfony etc.

What that can mean is that 404 requests for static assests are causing PHP to fire up and process the request and return a 404. If that happens a lot, for whatever reason, it can represent a significant server load.

Instead you can do this

# make image/asset 404s get an nginx 404 rather than be handled by PHP
location ~* .(png|jpeg|ico|gif|css|js|webp|avif) {
    try_files $uri =404;
}

# now handle everything else with a PHP fallback
location / {
    try_files $uri $uri/ /index.php$is_args$args;
}

What this will do is for any request with an extension in the list png|jpeg|ico|gif|css|js|webp|avif then nginx will either server the file, or will return a normal Nginx 404. This will happen very quickly and PHP will not be bothered with the request at all.

Then any requests which are not for static assets will be handled with the normal block.

This is the kind of tiny optimsation that, along with many other sensible choices and optimisations, can add up to significantly better performance and lower hosting costs.

Get in touch today

Send me a message concerning your idea, problem, or question – and I’ll personally respond as soon as possible.

Connect with me on LinkedIn.