Search code examples
phpcomposer-php

Should you use @dev or `dev-main` in composer JSON for local packages?


TL;DR: Should I use @dev or dev-main in my composer.json for local packages?


In our project we have a central composer.json which includes all the dependencies needed including local ones - code included in the git repo but isolated out as a separate composer packages.

We have a local folder set up as a repository:

{
    "repositories": [
        {
            "type": "path",
            "url": "./app/*/*"
        }
    ]
}

I read somewhere that we should include the dependencies with the @dev syntax - e.g.

{
  "require": {
      "app/local": "@dev"
  }
}

However, this stores the current branch in the composer.lock, so when a feature branch gets merged, the lock file references a non-existent branch until the next composer update is run. Is this ok?

I like the idea of @dev as it signifies which packages are local, but I don't like that a non-existent branch could be referenced.


Solution

  • TL;DR: Neither! Show it's born here, the star: *


    You ask:

    Should I use @dev or dev-main in my composer.json for local packages?

    And @dev or dev-main are meant as (part?) of a version constraint as in a requirement like the following:

    { "require" :
      { "app/local" : "@dev"
      }
    }
    

    And while the package app/local is from a Composer path repository:

    { "repositories" :
      [
        { "type" : "path"
        , "url"  : "./app/*/*"
        }
      ]
    }
    

    The answer is: Neither, there is no need for both, and in case it is easily misunderstood what they mean ("I like the idea of @dev as it signifies which packages are local"), it is often better to not practice adding to the confusion, but reducing the complexity and learn about the things their original meaning (enjoy the design, don't step over it).

    IMHO the much shorter and better speaking version constraint is the star/sun (asterisk) symbol "*" to denote its birthplace is local, and you're in or close to the centre of it.

    Caution

    This should go without saying that dev-main denotes a non-version-able branch referencing (never stable) version constraint while @dev is a stability flag in a package link, so is different to the asterisk (in this isolated form currently undocumented for the meaning within version constraints), and therefore you can still use them, .e.g. @dev for non-local packages (perhaps an option you're missing to see so far, but a documented case).

    So not using dev-main or @dev allows to use them additionally to * when they're (more) applicable. Perhaps the freedom you're looking for [email protected].

    Consider using Composer 2 as the repositories are canonical then; with Composer 1 they aren't.

    For all practicalities in this answer, Packagist and Composer networking are disabled. [Composer & PHP configuration below.]

    Rationale

    For a Composer path repository,

    If the package is a local VCS repository, the version may be inferred by the branch or tag that is currently checked out. Otherwise, the version should be explicitly defined in the package's composer.json file. If the version can't be resolved by these means, it is assumed to be dev-master. ¹

    As you have stated the precondition that the repository itself is within the projects own Git repository ("[...] in the git repo [...] as [...] separate composer packages" ²), all packages from that path repository are part of the local VCS repository which roots in the root package //project/composer.json side-by-side to //project/app/*/*/... and //project/.git; for Composer this results in the meaning of the block-quote above.

    The version constraint that reflects the package being local then certainly is: ¹⸴³

    composer.json#/require/app~1local "*"
    

    As you're using Git for version control, and those packages are local (only), I see absolutely no requirement to:

    • Either mark those packages as local – that should be obvious by their package name (e.g. "app/local", the example given by yourself) or at least by their repository (it is a path repository)
    • Nor mark them having development stability – the checked-out version will denote that (tag or branch name) or the dev-master fallback by Composer itself.

    There is also no confusion about the lock file then. It will always represent the state of the local system, and if there is an update, you will have an update. (Invalidations are flagged by dangling symbolic links.)

    Within the vendor folder, you should find those symbolic links, therefore, there is not even a requirement to run composer-update(1) when the repositories-json rooted ./app path repositories packages change as they're in the projects git work tree, so the tree is always leading.

    Composer supports such a scheme by the reference path repository option: ³

    composer.json#/respositories/0/options/reference "none"
    

    And (the more explicit as true is the preferred auto-default) symlink path repository option: ³

    composer.json#/respositories/0/options/symlink "true"
    

    I recommend making both explicit in that file.

    Notes on Exporting the Project

    To export your main project (the root package), use composer-archive(1) or do any other form of appropriate packaging, as the with local repositories Composer will not be able to acquire those packages on a system without those repositories configured (which could be any other system than this local one).

    It's a bit superfluous to state this explicitly, as this is true with any Composer project that installs/updates in an unpredictable WAN infrastructure.

    (and I need a lame excuse as I have not tested composer-archive(1) with this)


    Sunrise Example

    To be able to use * instead of @dev⁴ the packages' version has to be hinted – due to Composers default (and adequate) default stability – via the composer.json#/repositories/0/options/versions³ JSON Object that has property names as package names and the version as its properties' value:

    { "repositories" :
        { "type"    : "path"
        , "url"     : "./app/*/*"
        , "options" :
          { "versions" :
            { "app/local" : "0.0.0" }
          }
        }
    }
    

    For example, here the version "0.0.0".

    This now allows to use * from the get-go and versioning on the packages themselves apart from the main repositories tagging.

    Note:   A /version³ within any actual app/*/*/composer.json package does override, but likely there is some use in indexing the versions centrally within the root package. (For the answers' example it helped me.)

    Rundown:

    $ rm -rf vendor composer.lock
    $ cat composer.json
    {
      "name": "app/root",
      "description": "app/root",
      "license": "AGPL-3.0-or-later",
      "repositories": [
        {
          "type": "path",
          "url": "./app/*/*",
          "options": {
            "versions": {
              "app/local": "0.0.0"
            }
          }
        }
      ]
    }
    $ composer require app/local '*'
    ./composer.json has been updated
    Running composer update app/local
    Loading composer repositories with package information
    Updating dependencies
    Lock file operations: 1 install, 0 updates, 0 removals
      - Locking app/local (0.0.0)
    Writing lock file
    Installing dependencies from lock file (including require-dev)
    Package operations: 1 install, 0 updates, 0 removals
      - Installing app/local (0.0.0): Symlinking from ./app/app/local
    Generating autoload files
    

    Naturally, the version "0.0.0" is exemplary, but remains compatible with Semantic Versioning; remind, it is that Composer considers 0.x versions stable.

    Therefore, there is no requirement any longer to (ab)use the "@dev" stability flag in Composer Schema Package Links only to express that there is a package and that it's local. These will always lead as from the same repository, first file-system, then Git and finally Composer.

    Alternative Requirement Version Constraint

    The alternative, perhaps less expressive but more tracking, is to have Composer choose the requirement:

    $ composer require app/local
    ...
    Using version ^0.0.0 for app/local
    

    You get the Semantic Versioning leaning caret version constraint by its version, not the star – by the choice of Composer at the state of the .git repository.

    Note:   Caret (^) Notation:

    ^0.0.0 – potential version range 0.0.0... excluding 0.0.0.999..., same as excluding 0.0.1.

    This is per Composers' acting with pre-1.0 versions safety-in-mind. To give the different example for comparison:

    ^1.0.0 – potential version range 1.0.0... excluding 1.999... .999... .999... .999..., same as excluding 2.0.0.

    Now imagine and understand how you can make use of this in such a setup to incubate local only packages by controlling the version in root, in package, ... . @dev stands in your way then right from day one for local packages.

    Alternative Rundown:

    $ rm -rf vendor composer.lock
    $ git checkout -- composer.json
    $ composer require app/local
    ./composer.json has been updated
    Running composer update app/local
    Loading composer repositories with package information
    Updating dependencies
    Lock file operations: 1 install, 0 updates, 0 removals
      - Locking app/local (0.0.0)
    Writing lock file
    Installing dependencies from lock file (including require-dev)
    Package operations: 1 install, 0 updates, 0 removals
      - Installing app/local (0.0.0): Symlinking from ./app/app/local
    Generating autoload files
    Using version ^0.0.0 for app/local
    

    Both rundowns with

    Composer version 2.6-dev+f605389dc3101c903081ed0e95f9a872a8bfc930 (2.6-dev) 2023-08-16 12:05:14

    PHP 8.2.9 (cli) (built: Aug 16 2023 19:49:37) (NTS)


    References/Footnotes

    ¹ Composer path repository; cf. https://getcomposer.org/doc/05-repositories.md#path


    ² I received your wording with less clarity and therefore took the opportunity to add some to the extent declaring it such a local VCS repository, let me know if that was a misinterpretation; Cf. https://stackoverflow.com/revisions/76904227/1 . clarified per comment:

    To clarify, the packages are local, but are all part of the same git repository as the main application, the local packages themselves do not have separate repositories

    – it didn't change anything how it works, Composer supports the single repository form. (-- hk)


    ³ JSON Pointer notation for brevity; JavaScript Object Notation (JSON) Pointer; RFC 6901; https://www.rfc-editor.org/rfc/rfc6901


    I don't recommend – for a single git repository – to use the @dev stability flag to denote the package is for development – as I'd like to stabilize during development phases – and prefer branch aliasing instead. It allows me a better symmetry between the repository package version options and signals the main git repositories main branch (could also be made use of in the root package).