Today, I came across Saeghe — new package manager for PHP. Official site describes it as “modern PHP package manager that boosts your code”, let’s check it out!

Preparing environment

Saeghe is installed globally in the system (Linux and MacOS are supported), so it requires PHP to be installed too, obviously. For people working with the code long enough, it may be something natural, but today, in virtualisation-oriented times it seems like something obsolete. But OK, let’s do this old school 😉.

Documentation says about two possible installation methods: manual and with installer script. I used the latter, so I run:

bash -c "$(curl -fsSL"

Up to this point everything went correctly:

Saeghe installation with installer script

Indeed, saeghe binary file was available in my CLI (after terminal restart), so installer properly added tool’s directory to my $PATH. Going further with “Getting started” documentation, I configured GitHub token. If you ever encountered problems with API limits while installing Composer dependencies, you know it’s required in large projects. This step also went without any problems: saeghe credential $GITHUB_TOKEN properly saved my token in Saeghe’s configuration file (~/.saeghe/saeghe/credentials.json).

Saeghe credentials configuration

Saeghe in action

Migrating from Composer

I took my plugin for #Rector, which is small and is perfect for such experiments. Saeghe comes with migration command, so I run saeghe migrate and I got an error:

saeghe migrate

Warning: opendir(<project_path>/rector-money/vendor/roave/security-advisories): Failed to open directory: No such file or directory in ~/.saeghe/saeghe/Source/Commands/Migrate.php on line 131

Well, it looks like Saeghe does not support meta-packages, which don’t contain any actual files, like roave/security-advisories (which you should use 😉). Ok, so for the sake of the experiment, let’s remove it… After that, migration command worked properly, and in my project’s directory Packages directory appeared along with 2 files:


  "map": {
    "Codito\\Rector\\Money": "src"
  "entry-points": [],
  "excludes": [
  "executables": [],
  "packages-directory": "Packages",
  "packages": {
    "https:\/\/\/phpstan\/phpstan.git": "1.9.0",
    "https:\/\/\/rectorphp\/rector.git": "0.14.6",
    "https:\/\/\/moneyphp\/money.git": "v4.0.5",
    "https:\/\/\/phparkitect\/arkitect.git": "0.2.32",
    "https:\/\/\/phpstan\/extension-installer.git": "1.2.0",
    "https:\/\/\/phpstan\/phpstan-strict-rules.git": "1.4.4",
    "https:\/\/\/sebastianbergmann\/phpunit.git": "9.5.26",
    "https:\/\/\/symfony\/dependency-injection.git": "v6.1.5",
    "https:\/\/\/symplify\/easy-coding-standard.git": "11.1.16",
    "https:\/\/\/webmozarts\/assert.git": "1.11.0"

saeghe.config-lock.json (fragment):

  "packages": {
    "https:\/\/\/phpstan\/phpstan.git": {
      "version": "1.9.0",
      "hash": "e08de53a5eec983de78a787a88e72518cf8fe43a",
      "owner": "phpstan",
      "repo": "phpstan"

I think those files’ naming could be better, personally I would go with saeghe.json and saeghe.lock.json (or just saeghe.lock). Power in simplicity, also it would be more consistent with what we already know from Composer. This is small detail only, there are other issues too, but will come back to them later…

Installing dependencies

After migration I wanted to see how saeghe install behaves, but unfortunately it did not work and I got errors:

Warning: rename(<project_path>/rector-money/Packages/phpstan/phpstan-phpstan-ed473a6,<project_path>/rector-money/Packages/phpstan/phpstan): Directory not empty in ~/.saeghe/saeghe/Source/Git/GitHub.php on line 149)

I removed Packages directory, run command again and this time it finished correctly, but it took 52 seconds.

Updating dependencies

Command used for updating packages is similar to Composer’s, but it works differently because it requires packages’ repo URL, not the name. Also, it’s not possible to update all packages at once. We use saghe update like this:

saeghe update{owner}/{repo}.git --version={version-tag}

Unfortunately I wasn’t able to check how it works because I got errors all the time:

saeghe update
  Warning: file_get_contents(<project_dir>/rector-money/Packages/phpstan/phpstan/saeghe.config.json):
  Failed to open stream: No such file or directory in ~/.saeghe/saeghe/Source/FileManager/FileType/Json.php on line 7

It looks like Saeghe supports only repositories that… already use Saeghe 🤷‍♂️.

Building application

When I tried saeghe build, I got errors too, but at some point I managed to get it running. Build took 43 seconds, even 59 seconds in subsequent run… I think it’s too long considering so small project, and a really good hardware it was executed on (MacBook Pro M1).

Nevertheless, builds/development was created with file structure that was actually my project’s files. Difference between vendor and build/development/Packages directories was only 0.04MB. So what I gain? 🤔

Building in real time (watcher)

In principle, this command is to react in real time to changes in source files, and generate output files - a mechanism known from many tools, such as Hugo used on my blog. Cool theory, but not necessarily practice.

Running saeghe watch resulted in a wall of the same alerts as with update, so I was not able to get familiar with this functionality.

Developer Experience

Of course, a short adventure with Saeghe does not allow me to make a final opinion about this manager, but even after such a short time I was able to notice many shortcomings and/or problems:

No support for GITHUB_TOKEN

Saeghe needs a token for GitHub, but I believe that instead of storing it in yet another configuration file, it could just support setting token using the GITHUB_TOKEN environment variable (which is already used for e.g. GitHub CLI configuration).

No support for short commands

There is a mechanism in Composer that makes commands available under the shortest possible, unique aliases. Since update is the only command that begins with u, it is possible to use it as composer u (or even c u when using an alias for composer). It is a very convenient functionality that significantly minimises the need to tap the keys.

Saeghe does not have this option, so you have to type complete commands every time, e.g. saeghe update (or s update if you are using the alias mentioned at the beginning).

Inconvenient update mechanism

As mentioned earlier, to update packages we need the URL to the repository from which the package comes. We don’t expect to know these addresses by heart, so there is a need to do a copy/paste. Unfortunately, Saeghe stores these URLs in a way that prevents it, for example: https:\/\/\/rectorphp\/rector.git. Sure, there’s a chance we’ll have a command like this in our shell’s history, but personally, I find this interface just awkward.

In Composer, the vendor/package format and going through one abstraction layer (Packagist) is not a whim, but a well-thought-out mechanism by which:

  • packages are independent of their physical location: the author can migrate the code, and it is nearly unnoticeable for end users
  • the risk of name collisions is practically eliminated: each vendor can name their packages freely within their space (so there can be e.g. foo/collections and bar/collections)
  • dependency operations (adding, updating, deleting etc.) are based on the package name, which is much easier to remember and use in commands

In this context, Saeghe seems to swim against the tide, but may drown as a result 😉.

Managing package versions

In saeghe.config.json, we define packages, which are the packages to be installed. Unfortunately, unlike in Composer, you cannot use ranges here, each package requires the exact version (tags with the v prefix or without it, depending on the projects’ convention). I think this is a big step backwards - the idea behind the constraints in Composer is to specify the minimum required version once and then only update it. I can’t imagine manually changing the package version every time, executing a URL-based command, etc. Poor.

No cache for packages

After emptying Packages, it took 52 seconds to install my dependencies. Every time you delete Packages, all packages are downloaded again, and it takes about the same time. For comparison, Composer stores downloaded versions of packages in a cache, so even after removing the vendor directory and running composer install, the installation is instant, because packages are taken from the cache. It works globally - we download the same package only once in a given version, and it is installed with the cache in each project.

Semi-automatic migration

I mentioned that the migration went smoothly, however there are small details that required manual fixes too. The saeghe migrate command does not add both Packages and builds to .gitignore, so after such a migration, Git informs us of hundreds or even thousands of new untracked files.

Simplified owner-repo convention

As you can see above, in saeghe.config-lock.json there is a list of currently installed packages and their versions. However, the greatly simplified approach to the origin of the package is puzzling: each has the owner and repo fields. This is based on the assumption that the repository comes from a location that has two levels of nesting. You can see that the author of Saeghe did not deal with Gitlab, in which project groups can be nested multiple times, which would result in addresses like - so, what is owner, what is repo here 🤔?

Looking at the functional implementation of communication with Github and the rigid implementation of repository I wonder if the author foresaw sources other than GitHub, but that’s a completely different story…

PHP version requirement

I have not noticed anywhere in Saeghe the possibility of defining the required PHP version, which means that we are not able to force the required version of the runtime.

Dependency conflict resolution

Does not exist. The exact versions of packages that are defined in saeghe.config.json are installed, and whether they work together is a completely different story…

Poor CLI interface

Saeghe: CLI interface

Saeghe has a CLI interface, but it is very clunky. As mentioned above, error handling in the commands leaves much to be desired, and the commands themselves are not convenient to use due to the lack of verbose mode or even the lack of help (calls with --help do not offer any additional information on how the command works).

Compared to the commands in CLI applications based on symfony/console, this interface is just plain and unfriendly.

Composition and saeghe build

Reading Saeghe’s documentation, I wonder what the support for composition in the code looks like… Since build generates code consisting only of files that are used, is there a risk that the concrete implementations will be cut during building?

Imagine that there are interface Foo {}, class Bar { public method __construct (private Foo $foo); } and class Baz implements Foo {}. Instance of the Baz class is injected into the constructor of the Bar class by the Dependency Injection container, and in the whole Bar class we only operate on the Foo interface, not even knowing what its implementation is - what will Saeghe do with it? Especially when the container configuration would be in YAML, and not in a PHP file? 🤔

Maybe the answer to this question will come after publication 😉


Saeghe seems to be an unstable and not fully thought-out experiment so far. I was not able to fully test it, and maybe I do not fully understand it, but the number of problems and bugs I have encountered in this short time make me question the sense of using Saeghe in real projects. Interestingly, the latest version is 1.6, but I personally think it should be rather 0.1.6… For comparison, #PHPStan 0.12 (not supported, the current version is 1.x), or #Rector 0.14 are powerful tools incomparably more advanced than Saeghe at this point.

Personally, I think Saeghe should still be at 0.x, develop slowly, shape the public API, and listen to the feedback from the PHP community. It is definitely not a stable tool that is ready for commercial use.

However, I wish the author perseverance and good luck 🙂