Building a PHP CLI tool using DDD and Event Sourcing: distribution
Last updated: 2023-07-10 :: Published: 2023-07-10 :: [ history ]You can also subscribe to the RSS or Atom feed, or follow me on Twitter.
Although this post is part of the Building a PHP CLI tool using DDD and Event Sourcing series, you can also read it as a step-by-step guide to distributing a Laravel Zero application.
Distribution is about meeting your users where they are. The more options you offer, the broader public you can reach.
When it comes to PHP CLI tools, various distribution channels are available:
- Composer (via Packagist);
- PHP archives (phars);
- Containers (e.g. Docker);
- Homebrew;
- Scoop;
- Some other ways I'm probably unaware of.
Dime's distribution strategy relies on PHP archives, or phars. Dime's users can either download the phar and use it as-is, instal it via Composer, or run it through a Docker container.
This post explains how I set up Dime to leverage and automate these distribution channels, laying out steps that can be applied to any Laravel Zero application.
Dime? If you are new to this series, Dime is a free and open-source command-line tool written in PHP to help people calculate their crypto-related taxes in the UK.
In this series
- Why?
- The domain
- The model
- Software design
- Setting up Laravel Zero
- Getting started with EventSauce
- Distribution ⬅️ you are here
In this post
Application configuration
While most Laravel Zero applications can generate phars out-of-the-box, a few things can be done to optimise the result. The development and production (the app's distribution mode) environments are also likely to require separate configurations.
Hiding commands
This is what most Laravel Zero application menus will look like by default:
Dime v0.1.2
USAGE: dime <command> [options] [arguments]
help Display help for a command
migrate Run the database migrations
process Process a spreadsheet of transactions
review Display a tax year's summary
test Run the application tests
tinker Interact with your application
app:build Build a single file executable
app:install Install optional components
app:rename Set the application name
db:seed Seed the database with records
db:wipe Drop all tables, views, and types
make:command Create a new command
make:factory Create a new model factory
make:migration Create a new migration file
make:model Create a new Eloquent model class
make:seeder Create a new seeder class
make:test Create a new test class
migrate:fresh Drop all tables and re-run all migrations
migrate:install Create the migration repository
migrate:refresh Reset and re-run all migrations
migrate:reset Rollback all database migrations
migrate:rollback Rollback the last database migration
migrate:status Show the status of each migration
A lot of these commands are mostly used for development and should not be available to end users. Thankfully, Laravel Zero provides a simple way to conditionally add and remove commands, through the app/commands.php
configuration file.
Here is an extract from Dime's:
<?php
// ...
return [
// ...
'hidden' => array_merge(
[
NunoMaduro\LaravelConsoleSummary\SummaryCommand::class,
Symfony\Component\Console\Command\DumpCompletionCommand::class,
Illuminate\Console\Scheduling\ScheduleRunCommand::class,
Illuminate\Console\Scheduling\ScheduleListCommand::class,
Illuminate\Console\Scheduling\ScheduleFinishCommand::class,
Illuminate\Foundation\Console\VendorPublishCommand::class,
LaravelZero\Framework\Commands\StubPublishCommand::class,
],
Phar::running() ? [
Illuminate\Database\Console\Migrations\FreshCommand::class,
Illuminate\Database\Console\Migrations\InstallCommand::class,
Illuminate\Database\Console\Migrations\MigrateCommand::class,
Illuminate\Database\Console\WipeCommand::class,
] : [],
),
// ...
'remove' => array_merge(
[],
Phar::running() ? [
Illuminate\Database\Console\Factories\FactoryMakeCommand::class,
Illuminate\Database\Console\Seeds\SeedCommand::class,
Illuminate\Database\Console\Seeds\SeederMakeCommand::class,
Illuminate\Database\Console\Migrations\MigrateMakeCommand::class,
Illuminate\Database\Console\Migrations\RefreshCommand::class,
Illuminate\Database\Console\Migrations\ResetCommand::class,
Illuminate\Database\Console\Migrations\RollbackCommand::class,
Illuminate\Database\Console\Migrations\StatusCommand::class,
Illuminate\Database\Console\WipeCommand::class,
Illuminate\Foundation\Console\ModelMakeCommand::class,
LaravelZero\Framework\Commands\BuildCommand::class,
LaravelZero\Framework\Commands\InstallCommand::class,
LaravelZero\Framework\Commands\MakeCommand::class,
LaravelZero\Framework\Commands\RenameCommand::class,
LaravelZero\Framework\Commands\StubPublishCommand::class,
LaravelZero\Framework\Commands\TestMakeCommand::class,
] : [],
),
];
There are two arrays – hidden
and remove
. Commands in the hidden
array won't show in the application's menu, but they can still be run by the system. On the other hand, commands in the remove
array won't be registered at all.
The reason we need both is because some commands that should not be available to end users are still needed by the application itself. For instance, Dime will reset the database every time the process
command is called, which under the hood runs the migrate:fresh
, db:wipe
and migrate
commands.
To make sure these commands are still available for development even though they won't show up in production, we check the application's context, here provided by the Phar
class. The Phar::running
method returns the path to the phar currently being executed, which is an empty string when the command was not run through a phar.
In other words, when the method returns an empty string, we are not in production.
Once the commands are properly configured, this is what Dime's menu looks like when displayed through the phar:
Dime v0.1.2
USAGE: <command> [options] [arguments]
help Display help for a command
process Process a spreadsheet of transactions
review Display a tax year's summary
Production database
While the development database can live in the database
folder without any issue, a PHP archive cannot update the files it contains, so the production database needs to be located elsewhere, outside the phar.
This is Dime's production database configuration from config/database.php
:
<?php
// ...
'production' => [
'driver' => 'sqlite',
'url' => '',
'database' => $_SERVER['HOME'] . '/.dime/database.sqlite',
'prefix' => '',
'foreign_key_constraints' => true,
],
$_SERVER['HOME']
resolves to the user's home directory (the one you can access via the ~
shortcut, e.g. /Users/alice
), meaning the SQLite database is located in the ~/.dime
folder (e.g. /Users/alice/.dime/database.sqlite
).
The rest of the database configuration is the same as the local
database.
Let's now see how to ensure the right database is picked up based on the current environment.
DotEnv add-on
Before distributing Dime, the only environment I had to deal with was local
, so there was no need to worry about other configurations. With the introduction of a production environment, however, I needed a way to manage separate setups.
Most framework users will be familiar with DotEnv, a library to parse and load environment variables in PHP applications.
In Laravel Zero, it comes as an add-on:
$ php dime app:install dotenv
This command will load the library and create a .env
file with some example entries.
I set Dime in a way that I only need a .env
file for the local environment, with values that override the default ones, which are optimised for production.
In other words, I don't need to provide a production-specific .env
file, which simplifies the build process.
I also set up Dime to match the database and environment names, meaning I only need a single entry in the local .env
file:
APP_ENV=local
This environment variable needs to be set to production
by default in two configuration files – first, in config/app.php
:
'env' => env('APP_ENV', 'production'),
And also in config/database.php
:
'default' => env('APP_ENV', 'production'),
And that's all there is to the production database's configuration. We're not done yet though – we now need to make sure the database.sqlite
file always exists in ~/.dime
, or the application won't run.
Database manager
The database migration commands require the database to exist before we can run them, meaning we somehow need to make sure the ~/.dime/database.sqlite
file is always present.
In Dime, this is handled by the DatabaseManager
service, whose logic roughly goes like this:
<?php
// ...
class DatabaseManager
{
// ...
public function prepare(): void
{
// Figure out the database connection's configuration path
$entry = sprintf('database.connections.%s.database', $this->config->get('app.env'));
// Retrieve the database configuration
$file = $this->config->get($entry);
// Create the database file if it doesn't exist
if (! is_file($file)) {
$directory = dirname($file);
// Create the `~/.dime` directory first if it doesn't exist
if (! is_dir($directory)) {
$this->process->run(sprintf('mkdir -m755 %s', $directory));
}
// Create the file
$this->process->run(sprintf('touch %s', $file));
}
// Migrate the database quietly
$this->artisan->call('migrate:fresh', ['--force' => true], new NullOutput());
}
}
$this->config
, $this->process
and $this->artisan
are injected instances of Illuminate\Contracts\Config\Repository
, Illuminate\Process\PendingProcess
and Illuminate\Contracts\Console\Kernel
, respectively.
The PendingProcess
class comes from Laravel's Process
component, which is based on Symfony's and was introduced in version 10.
NullOutput
comes from Symfony's Console Component and ensures no output will be shown in the console.
As for the rest, the comments should be self-explanatory.
The DatabaseManager
service guarantees that the database will always be there, and since this was the last production requirement, Dime is now distribution-ready.
Let's now explore the various distribution channels it uses and how to set them up.
Distribution through a PHP archive
Laravel Zero makes it super easy to turn your application into a phar. In fact, in most cases you'll be able to do it out-of-the-box, with this command that will generate an archive in the builds
folder at the root:
$ php <your-app-name> app:build <your-build-name>
In some cases, however, you will need to make a few changes to make the phar more user-friendly and simplify its distribution.
Include the relevant folders
Laravel Zero uses the Box library under the hood to build phars. This package comes with a box.json
configuration file that will work with most applications but that requires a few changes in some cases.
For instance, Dime needs the database
and domain/src
folders to be included in the build:
{
"chmod": "0755",
"directories": [
"app",
"bootstrap",
"config",
"database",
"domain/src",
"vendor"
],
"..."
}
That's because using a database is optional in Laravel Zero and comes as an add-on, meaning the database
folder is not included by default. And because Dime was developed following a DDD approach, it has an extra domain
folder that needs including too.
Self-update
Another optional feature is the possibility for the phar to update itself if a new version is available.
This also comes as an add-on:
$ php dime app:install self-update
Once installed, a new self-update
command will appear in the application's menu.
Different strategies can be used to download the latest archive from various sources, the default one being to look inside the builds
folder on GitHub.
I went for a custom strategy for Dime, which downloads the phar from the latest release's assets (I'll get back to this).
To use it in place of the default one, you first need to publish the configuration file:
$ php dime vendor:publish --provider "LaravelZero\Framework\Components\Updater\Provider"
And then update the strategy in the config/updater.php
file:
'strategy' => Strategy::class,
Release workflow
Dime's releases are tagged following the Semantic Versioning scheme. Each release comes with an updated phar, to be downloaded from the release's assets.
If we were to create a release manually, this is what we would do:
- Build and commit the phar;
- Create a new tag;
- Draft a new release;
- Upload the phar as a release asset; and
- Publish the release.
All these steps can be automated.
Dime uses a GitHub workflow for this, simplified below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | name: Release
on:
workflow_dispatch:
inputs:
version:
description: 'Release version'
type: string
required: true
env:
APP_ENV: "local"
jobs:
phar:
name: Phar
runs-on: ubuntu-latest
steps:
- name: Validate tag
uses: FidelusAleksander/gh-action-regex@v0.2.0
with:
regex_pattern: "^v\\d+(\\.\\d+)?(\\.\\d+)?$"
regex_match_type: match
text: ${{ github.event.inputs.version }}
- name: Checkout code
uses: actions/checkout@v3
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: "8.2"
extensions: "bcmath"
coverage: "none"
- name: Install dependencies
uses: ramsey/composer-install@v2
- name: Build archive
run: "php dime app:build dime --build-version=${{ github.event.inputs.version }}"
- name: Push changes
uses: stefanzweifel/git-auto-commit-action@v4
with:
branch: main
commit_message: "Updated phar"
- name: Draft release
uses: softprops/action-gh-release@v1
with:
name: ${{ github.event.inputs.version }}
tag_name: ${{ github.event.inputs.version }}
draft: true
files: ./builds/dime
generate_release_notes: true
|
The workflow is triggered manually from the repository's Actions tab and takes the release's version as input:
The workflow consists of a single job, with the following steps:
- Validate the tag using a regular expression;
- Check out the code;
- Set up PHP with the right version and extensions;
- Instal the Composer dependencies;
- Build the archive using the submitted value as the version; and
- Draft the release.
That last step also uses the version as the release name and as the release's tag, and generates the release notes and uploads the phar as a release asset.
Also note the presence of the APP_ENV
environment variable at the top, set to local
. Laravel Zero won't build the archive if the application is in production mode, which is the case by default because of a change we've made in a previous section. So we override it here.
Once the workflow completes, the new archive can be downloaded from the latest release and used straight away:
$ php dime
The recommended approach, however, is to make the file executable and move it to a directory that's in the user's system path:
$ chmod +x dime
$ mv dime /usr/local/bin/dime
After doing so, the application can be invoked from anywhere running dime
.
Distribution through Composer
If you've read some of my previous articles, you may be familiar with the distribution of command-line tools and packages through Composer and Packagist.
The gist of it is that, since we don't know much about the systems our work will end up on, it's worth making sure it is compatible with several dependency versions, to accommodate as many clients as possible.
The good news is that phars partially eliminate that need because they include (and isolate) the package's dependencies. In practice, this means we don't need to worry about another PHP application using a dependency version conflicting with ours.
That being said, phars still rely on the system's PHP version and installed extensions, so there is a limit to what can be isolated.
The other good news is that Laravel Zero supports distributing phars through Packagist and Composer almost natively, with just a few tweaks.
Composer configuration
Laravel Zero's default Composer configuration doesn't work with phars out-of-the-box.
First, the path to the binary needs to point to the archive in composer.json
:
{
"...",
"bin": ["builds/dime"],
"..."
}
Then, all dependencies but the supported PHP versions and required extensions must be moved to require-dev
:
{
"...",
"require": {
"php": "^8.2",
"ext-bcmath": "*"
},
"require-dev": {
"brick/date-time": "^0.4.2",
"eventsauce/eventsauce": "^3.4",
"eventsauce/message-repository-for-illuminate": "^0.4.2",
"eventsauce/object-hydrator": "^1.3",
"eventsauce/pest-utilities": "^3.4",
"fakerphp/faker": "^1.21.0",
"illuminate/database": "^10.13",
"intonate/tinker-zero": "^1.2",
"laravel-zero/framework": "^10.0",
"..."
},
"..."
}
The reason for this is that, since our application's dependencies are already included in the phar, we don't need Composer to instal them again. Moving the dependencies to require-dev
is a way to ensure it won't.
But how do we make sure that Box will include them in the archive?
Box configuration
As mentioned earlier, Laravel Zero relies on Box to generate phars from applications. We've already made a couple of changes in box.json
, but we now need to make sure development dependencies are also included in the build.
We do this through the exclude-dev-files
attribute:
{
"...",
"files": [
"composer.json"
],
"exclude-composer-files": false,
"exclude-dev-files": false,
"..."
}
By moving all dependencies to require-dev
and setting exclude-dev-files
to false
, we ensure that the phar will include all the necessary dependencies while preventing Composer from installing them as well.
A downside to this approach is that actual development dependencies will also be included in the archive, however.
Exclude redundant files
When installing a PHP application through Packagist, Composer doesn't only instal the non-development dependencies – it also downloads the application's files.
Just like the dependencies, however, these files are already included in the phar, so Composer doesn't need to download them as well.
To make sure it doesn't, create a .gitattributes
file at the root of the application to list the content you wish to exclude.
Here is Dime's, for instance:
* text=auto eol=lf
/.docker export-ignore
/.github export-ignore
/app export-ignore
/bootstrap export-ignore
/config export-ignore
/database export-ignore
/domain export-ignore
/tests export-ignore
/.editorconfig export-ignore
/.env.example export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/box.json export-ignore
/composer.lock export-ignore
/dime export-ignore
/phpstan.neon.dist export-ignore
/phpunit.xml.dist export-ignore
/pint.json export-ignore
/rector.php export-ignore
Hide the self-update command
The build process is currently set up in a way that Dime's binary always exposes a self-update
command, although it should only be available when using the archive directly and not through Packagist or Docker.
The self-update
command essentially replaces the archive with a newer version of itself, but when using another distribution channel, we want that channel to be responsible for the update, and not the archive itself.
How do we ensure that this command is only available in the relevant context?
The solution lies within the config/commands.php
file again. Dime uses a helper class returning the archive's current context, which allows for deciding whether to include the command or not:
<?php
// ...
return [
// ...
'remove' => array_merge(
[],
Phar::running() ? [
Illuminate\Database\Console\Factories\FactoryMakeCommand::class,
Illuminate\Database\Console\Seeds\SeedCommand::class,
// ...
] : [],
Helpers::installedViaPhar() === false ? [
LaravelZero\Framework\Components\Updater\SelfUpdateCommand::class,
] : [],
),
];
The Helpers::installedViaPhar
method essentially checks that the archive is not currently being used via Composer:
<?php
// ...
public static function installedViaPhar(): bool
{
return str_contains(__DIR__, '.composer/vendor') === false;
}
Release and distribution
At this stage, Dime is Composer-ready and, since the release workflow is already taking care of building the archive, all that's left to do is register the application on Packagist.
Log into packagist.org (create an account if necessary) and head over to the submit section of the website. Paste in the URL of your application's GitHub repository and give Packagist a couple of seconds to process it.
You should now be able to instal the latest release:
$ composer global require osteel/dime
If the ~/.composer/vendor/bin
directory is already in your system's path, you can now invoke the binary from anywhere:
$ dime
Distribution through Docker
Phars can also be distributed through Docker images. With this method, Docker is the only dependency – no local versions of PHP or extensions are required.
Building the image
While the Laravel Zero documentation doesn't mention Docker images, the framework allows for building some through its reliance on Box:
$ ./vendor/laravel-zero/framework/bin/box docker builds/dime
This command reads the application's requirements from the archive's composer.json
file and generates the corresponding Dockerfile, including the right PHP version and extensions:
1 2 3 4 5 6 7 8 | FROM php:8.2-cli-alpine
COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
RUN install-php-extensions zlib bcmath
COPY builds/dime /docker/dime
ENTRYPOINT ["/docker/dime"]
|
While this is optional, I keep Dime's Dockerfile in a .docker
folder at the root of the application.
And this is the command to build the image:
$ docker build -t ghcr.io/osteel/dime:latest -f .docker/Dockerfile .
The -t
flag is to tag the image (ghcr.io/osteel/dime:latest
) and the -f
flag points to the Dockerfile's location.
I don't bother keeping track of previous image versions for Dime, so any new version gets the latest
tag and overwrites the previous one.
Container registry
Once the image is built, we need to distribute it. A common way to do this is to push it to a registry – Docker Hub is a popular one, but I decided to stick to GitHub and use its Container Registry.
The first thing to do is to log into the platform using an access token. There are two types of tokens – fine-grained personal access tokens and personal access tokens (PAT). The former are deemed more modern and secure, but at the time of writing they were still in beta and I couldn't get them to work with Dime's release workflow, so I went for a regular PAT instead.
The latter can be obtained from the developer settings and must be created with the following permissions: write:packages
, read:packages
and delete:packages
.
It's also possible to set an optional expiration date.
The generated token is then used as the password when logging into the container registry (the command will prompt for it):
$ docker login ghcr.io -u <USERNAME>
And this is how I push the image to the registry:
$ docker push ghcr.io/osteel/dime:latest
Once pushed, your image will show up as a package in the Packages tab of your GitHub profile:
You can link the package to the repository holding its source code from the package's settings, as well as change its visibility:
After which it will appear on the repository's page, under the Releases section on the right-hand side:
Release workflow
Like phars, any new release should come with an updated Docker image to be pulled from the container registry. And just like phars, this process can be automated.
This takes the form of a new job in Dime's release workflow, but before I give you the details, let's see how to add the PAT as a GitHub secret so the workflow can use it.
You need to head over to the repository's settings on GitHub and to the Secrets and variables menu on the left-hand side. It's got an Actions submenu that will take you to this form:
Create a new repository secret (here named GHCR_TOKEN
) and set the PAT as its value.
The workflow will then be able to use it to log into the registry and push images.
This is what the corresponding job looks like in Dime's workflow:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | name: Release
# ...
env:
APP_ENV: "local"
REGISTRY: "ghcr.io"
jobs:
phar:
# job details...
docker:
name: Docker
needs: phar
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Log into registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.repository_owner }}
password: ${{ secrets.GHCR_TOKEN }}
- name: Set up Buildx
uses: docker/setup-buildx-action@v2
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
file: .docker/Dockerfile
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ env.REGISTRY }}/${{ github.repository }}:latest
|
First, note the presence of the REGISTRY
environment variable at the top. A new docker
job has also been added, after the one building and committing the phar. It has a needs
key referring to the latter, ensuring that the docker
job only starts once the new phar is available.
The job consists of the following steps:
- Check out the code;
- Log into the container registry using the PAT as password and the repository owner's name as username;
- Set up Buildx to enable multi-platform images; and
- Build and push the image.
This last step also ensures the image is compatible with both AMD and ARM platforms and tags it by combining the registry name and repository name.
From there, any successful workflow run will update the latest
image using the new phar and push it to the container registry for everyone to download and use:
$ docker run -it --rm ghcr.io/osteel/dime:latest help
This would download the image, start a container based on it and pass on the help
command to the phar within the container.
Once the command is executed, the container is destroyed (--rm
flag).
Shell script
While end users can run docker
commands to use Dime, these are a bit verbose:
$ docker run -it --rm ghcr.io/osteel/dime:latest help
Things get worse when the transaction spreadsheet is involved and needs to be transferred from the user's computer to the container. The ephemeral nature of the container is also a factor, as whatever data it contains is likely to be lost at some point, including the SQLite database.
The solution to the above is to use bind mounts, a way to import local files or directories in a container.
Using this method, this is what running the process
command looks like:
$ docker run -it --rm -v ~/.dime/database.sqlite:/root/.dime/database.sqlite -v transactions.csv:/tmp/transactions.csv ghcr.io/osteel/dime:latest process /tmp/transactions.csv
Two bind mounts are being used here – one for the database and one for the transaction spreadsheet. While this works, this is terrible user experience.
The solution I came up with is to wrap these commands in a small shell script that essentially converts regular Dime commands to Docker ones which, in turn, are converted back to Dime commands inside the container.
Here is a simplified version of the script:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | #!/bin/sh
if ! [ -f "~/.dime/database.sqlite" ]; then
mkdir -p ~/.dime && touch ~/.dime/database.sqlite
fi
command="docker run -it --rm -v ~/.dime/database.sqlite:/root/.dime/database.sqlite"
if [[ "$1" == "process" && -n "$2" && -f "$2" ]]; then
filename=$(basename "$2")
path=$(cd $(dirname $2); pwd)/$filename
command+=" -v $path:/tmp/$filename ghcr.io/osteel/dime:latest process /tmp/$filename"
else
command+=" ghcr.io/osteel/dime:latest ${@:1}"
fi
eval "$command"
|
It first makes sure the database exists locally, and creates it otherwise. It then starts putting together the Docker command, starting with mounting the database file onto the container, since we need it for every command.
The rest of the script depends on the executed command – if it's process
, it mounts the transaction spreadsheet inside the container's tmp
folder, using the spreadsheet's absolute local path.
If it's another command, it passes it to the container along with any arguments.
Finally, once the docker
command is complete, the script executes it with eval
.
End users can download the shell script and use it instead of running complicated Docker commands:
$ sh dime.sh
For an even better user experience, the recommended approach is to make the file executable and move it to a directory that's in the local system path:
$ chmod +x dime.sh
$ mv dime.sh /usr/local/bin/dime
After which the application can be used from anywhere running dime
.
Closing thoughts
Distributing a PHP application can mean different things based on context.
Most developers only need to target a specific PHP version and usually freeze the application's dependencies through the composer.lock
file. The application is then deployed to some server and made available from there, over HTTP.
Package maintainers often aim at supporting various PHP and dependency versions and build their continuous integration workflows accordingly.
Building standalone command-line applications with PHP is less common and poses a different set of questions when it comes to distribution: who am I targeting? On which platforms? Should I assume PHP will be available?
PHP archives have been around for 20 years and, despite their portable nature and the fact that they pretty much solve the dependency hell out-of-the-box, they seem to be rarely used.
Phars are very flexible and allow for a variety of distribution channels – they can be downloaded and used as-is or installed through Composer and Docker as this article illustrates. But they can also be distributed through Homebrew, Scoop, and maybe soon as native programs.
I've learnt a lot writing this series, but if I were to pick a single takeaway, it would be that PHP is way more versatile than I ever realised. And I've been working with this language for more than 15 years.
This post concludes the Dime series – at least for now. I still have a fairly long list of improvements I'd like to make and some of them may constitute enough material to become blog posts.
You can subscribe to email alerts below so you don't miss future articles, be they about Dime or something else. You can also follow me on Twitter where I share my content as soon as it is available.
You can also subscribe to the RSS or Atom feed, or follow me on Twitter.