Fusion: Everything Is a Modular Package

php dev.to

What if there were no traditional static root projects and package types, allowing a package manager to build the root project along with its dependencies? To see how this generic approach benefits PHP projects and which problems it solves, let's look at the core implementation of Fusion's modularity:

  • Projects are modular packages.
  • Packages can depend on each other and even themselves.
  • Packages are built the same way and share a unified state with per-package stateful files.

Recursive Dependency Graph

The ability for a package to depend on itself makes standalone packages manageable and removes the need for static initial distributions. Instead, root and nested packages are built within a single dependency graph:

Since Fusion itself is also a package, it works well as a real-world example:

In its own metadata, the package manager depends on a dependency injection container for abstraction, and for user-friendly non-breaking updates (2.x.x), it also depends on itself:

{"structure":{"valvoid.com/valvoid":"fusion/2.0.0","/dependencies":"valvoid.com/valvoid/box/2.0.0"}}
Enter fullscreen mode Exit fullscreen mode

Because these dependencies are defined in the metadata, the maintenance command remains user-friendly without arguments:

fusion build
Enter fullscreen mode Exit fullscreen mode

For major upgrades, for example for future release 3.0.0, an explicit reference argument must be passed to override the fallback reference defined in the metadata:

fusion build build.source=valvoid.com/valvoid/fusion/3.0.0
Enter fullscreen mode Exit fullscreen mode

Regardless of how the reference is passed, Fusion connects different versions of the same root package into a recursive graph. The higher version can either trigger a custom migration script or rely on Fusion's default behavior.

Open Dependency Graph

Without static root projects, graphs can expand upward. The flexible entry point, which shifts with each new reference, lets standalone packages become default dependencies that parent packages can consume and extend:

For example, Fusion itself is such a default package:

Determining which parent packages customize a default package, and in what order they are applied, is not trivial. For this reason, Fusion provides built-in extension logic at the metadata layer, which it also uses itself. The package manager exposes the virtual config path in production metadata via the extendable indicator and expects the generated extender data to be in the stateful directory:

{"structure":{"/config":"extendable","/state":"stateful"}}
Enter fullscreen mode Exit fullscreen mode

A custom package manager, as an extender package on top, declares Fusion as a dependency in its production metadata and maps its own configuration directory into the exposed virtual path:

{"structure":{"/config":":valvoid/fusion/config","/dependencies":"valvoid.com/valvoid/fusion/2.0.0"}}
Enter fullscreen mode Exit fullscreen mode

Since the relation is abstract, the extender does not pass anything directly to Fusion's configuration. Instead, it contributes to the shared state generated during the build process, constrained only by the config schema:

fusion build
Enter fullscreen mode Exit fullscreen mode

Once completed, Fusion, like any other default package, reads the ordered extender directories from the generated extensions.php file in its own stateful directory:

return [

    // exposed virtual path
    "/config" => [

        // mapped extender directories
        2 => "<absolute extender root dir>/config",
       #3 => "<other extender>",
    ]
];
Enter fullscreen mode Exit fullscreen mode

During subsequent executions, the package manager applies all mapped configs in deterministic top-down order without exposing merge logic to extenders, minimizing overhead.

Circular Dependency Graph

Due to unrestricted references, packages can depend on each other, resulting in a graph relative to its entry point. This allows the graph to be reused across different builds by constructing it from different nodes:

Reflex and Box are example packages that share such a graph:

Testing upcoming releases before your own release, such as breaking changes in the 2.0.0 dependency injection container used by Reflex for abstraction, is the most interesting part of such relationships. The default Valvoid registry
is production-only and limited to its own sources (valvoid.com/<path>/<reference>), so the Reflex production metadata fusion.json prepends the 2.0.0 reference pattern for upcoming versions using the logical || operator and applies the changes along with the next bugfix release 1.0.1:

{"version":"1.0.1","structure":{"/dependencies":"valvoid.com/valvoid/box/2.0.0||1.1.0"}}
Enter fullscreen mode Exit fullscreen mode

Inside the shared development metadata fusion.dev.php, Reflex drops the production source by setting it to null and defines the upcoming version 2.0.0 using the GitLab repository branch 2.0 as the offset:

return [
    "structure" => [
        "/dependencies" => [
            "gitlab.com/valvoid/box/'code/==2.0.0:2.0",
            "valvoid.com/valvoid/box" => null
        ]
    ]
];
Enter fullscreen mode Exit fullscreen mode

Box, in parallel, increases its version in the production metadata fusion.json to 2.0.0, and in the shared development metadata fusion.dev.php it also changes the source to the GitLab repository branch 1.0 as the offset:

{"version":"2.0.0"}
Enter fullscreen mode Exit fullscreen mode
return [
    "structure" => [
        "/dependencies" => [
            "gitlab.com/valvoid/reflex/'code/==1.0.1:1.0"

            // previous production source
            // "valvoid.com/valvoid/reflex/1.0.0"
        ]
    ]
];
Enter fullscreen mode Exit fullscreen mode

Using separated metadata files, both packages build each other for testing inside the pipeline with upcoming versions by running the build command:

fusion build
Enter fullscreen mode Exit fullscreen mode

Wrapping Up

These are the use cases I have found in my projects and implemented in Fusion. There will likely be more as it is used in additional projects and different setups. If you like the approach without traditional static root projects and package types, you can try Fusion in your next project:

Source: dev.to

arrow_back Back to Tutorials