Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@joelwurtz
Copy link
Contributor

@joelwurtz joelwurtz commented Mar 27, 2025

Ref #11635

Motivation

Some packages propose optional dependencies to add optional features to their library.

This is actually mainly implemented by :

  • Having suggest in your composer.json (but from what i remember this will be deprecated ? maybe i'm wrong there)
  • Having the dependencies in "require-dev"
  • Adding a conflict rule if necessary to avoid bad versions of the needed library
  • Adding documentation to tell user to "require" this dependency (or maybe multiple one) if he needs a specific feature
  • Add checks in your code to determine if library is present (either by looking if a class exists or by using the InstalledVersions file of composer to see if the dependency is installed)

Also when feature may require multiple dependencies some library author prefer to propose a intermediate package that have the necessary requires in order to offer a better DX for end user (no need to require multiple packages, better handling of versions, ...)

When doing this, library author often prefers to have a monolith repository and splitting directory into multiple git repository which allows to create PR that can update both the main library and the optional dependency without too much hassle

Let's be honest, the current way is complicated and add a lot of maintenance for library authors, this PR aims to add a new way in composer to handle this use case.

Goals

  • Allowing a library author to declare features on its package by telling : this feature require those packages in those versions
  • Allow a library user to declare which feature he want to use
  • Allowing a library author to better detect if a feature is enabled: a dependency may be installed but maybe it was done by another library and not because the user want this specific feature

Declaring features

In order for the author of a library to declare features he will now have to do add this lines in its composer.json file :

"feature": {
    "my-awesome-feature": {
        "description": "An optional description describing the feature",
        "require": {
             "vendor/packageA": "1.0",
             "vendor/packageB": "^2.0",
             ...
        }
    }
}

Using features

In order for a user to use a feature of a library he will now have to add this lines in its composer.json file :

"require": {
    "vendor/library": "^1.0",
},
"require-feature": {
    "vendor/library": ["my-awesome-feature"]
}

Detecting features

When the library want to detect if a feature is enabled it will now be able to do this in PHP :

<?php

use Composer\InstalledVersions;

// method exists check to avoid bc break old composer version
if (method_exists(InstalledVersions::class, 'hasFeature') && InstalledVersions::hasFeature("vendor/library", "my-awesome-feature")) {
   // handling my awesome feature
}

Testing features

New arguments are provided to the install and update commands in order for library authors to test their features.

Features dependencies of the root package (not of others packages unless required) are always resolved in the composer.lock file.

This ensure that given any combination the set of dependencies is always correct, like require-dev dependencies

However, like require-dev it is possible to avoid installing the package link to a feature

Some examples :

  • Will install require / require-dev / and all features packages
composer install
  • Will install require and all features packages
composer install --no-dev
  • Will install require and require-dev package (but no packages required by features)
composer install --no-features
  • Will install require, require-dev and packages of feature "my-awesome-feature"
composer install --feature my-awesome-feature

In order to achieve that packages are now splitted like this in the composer.lock files :

"packages": [...]
"packages-dev": [...]
"packages-feature": {
    "my-awesome-feature": [...],
    "other-feature": [...]
}

This allow library authors to install and test their packages with any possible combination of features :

  • testing without features
  • testing with only one of the features
  • testing with a specific set of features
  • etc ....

Errors

In order to provider better information for end user some rules where added to the SAT resolution :

  • Require a inexistant feature : It will throw an error where it tells user that this feature does not exist on the wanted library
  • Require a feature on a library that is not present : It will create a new error where it tells the user that the package is not present in its requirements

None goals

To keep this first version simple, some changes are not considered in this PR :

  • Having a feature that require another feature
  • Having a feature that provides other packages (would be nice, but can be done in another PR)
  • A default feature set and avoiding requiring default features
  • SAT resolution depending on feature (if i require a feature that is only available in a specific version it would be nice to restrict the version or tell user why its current set of dependencies is not possible). There is some rules for feature and SAT but right now the implementation is "stupid" (This has been done finally)
  • Simpler syntax for feature requirements, it would be nice to be able to declare feature requirement directly in the require section like the following :
"require": {
    "vendor/package": { "version": "^1.0", "features": ["my-awesome-feature"] },
}

However this syntax would bring a too big BC break (older versions of composer would not be able to understand this).

Things to do in this PR :

  • More tests, i added some but it definitely does not cover all uses cases
  • Add the hasFeature method in InstalledVersions
  • Fixing phpstan 😭

@joelwurtz joelwurtz force-pushed the feat/feature-system branch 3 times, most recently from ae84fa5 to 8b582b7 Compare March 28, 2025 08:48
@Seldaek
Copy link
Member

Seldaek commented Mar 29, 2025

oof, thanks but I am afraid that's gonna take me a while to digest :D

@joelwurtz
Copy link
Contributor Author

oof, thanks but I am afraid that's gonna take me a while to digest :D

There is no rush, it's a first implementation that should work, but since it's my first time really digging into composer i may have miss some details so take your times

@stof
Copy link
Contributor

stof commented Apr 4, 2025

Always resolving dependencies will all features that are not required would be a nightmare regarding dependency hell IMO, as it means that dependencies you don't want will still create conflicts with other packages in the solver.

@Seldaek
Copy link
Member

Seldaek commented Apr 4, 2025

Yeah I haven't looked yet at the implementation but this absolutely should be update-time only, you select which features you want when you update dependencies, these are then included in the requirements in the poolbuilder, and then at install time you just have a list of things to install like usual in the lock file, features have no impact at all at install time (except that probably the lock file needs a list of features to be able to dump this in InstalledVersions style fashion if we want that.. Although I'd argue class_exists() checks should maybe be preferred at runtime to see if the dependency you need is present.

@joelwurtz
Copy link
Contributor Author

joelwurtz commented Apr 4, 2025

I think my wording was wrong, it's not dependencies that are resolved with all features, it is only dependencies that are required by features of the root package to ensure that any combination of features for the root package is valid (since it's the worst case). It is the same thing that is done with require-dev packages.

Dependencies provided by a feature in another package are not resolved / fetched unless the user want it.

It will certainly prevent some possibilities (like i have a feature that allow to work with older packages) but this has been done to have something working as a first implementation, and avoid some complexity, it could always be achieved latter.

Example

Package A :

{
   "name": "vendor/a",
   "require": {
       "symfony/validator": "^7.0"
   },
   "feature": {
       "saas-provider": {
           "require": {
               "symfony/http-client": "^7.0",
               "symfony/serializer": "^7.0"
           }
       }
   }
}

My package (root package) that require A :

{
   "name": "my-library",
   "require": {
       "vendor/a": "^1.0"
   },
   "feature": {
       "a-shiny-feature": {
           "require": {
               "jolicode/automapper": "^9.0"
           }
       }
   }
}

In this case the composer.lock of the root package (my-library) will contain :

  • vendor/A
  • symfony/validator (and it's dependencies)
  • jolicode/automapper (since it's a require of a feature that i propose)

It will also be installed by default unless i restrict the installation with

composer install/update --no-features

In this case, it will still be in composer.lock but it will not be installed

However if i require the saas-provider feature of vendor/a library

{
   "name": "my-library",
   "require": {
       "vendor/a": "^1.0"
   },
   "feature": {
       "a-shiny-feature": {
           "require": {
               "jolicode/automapper": "^9.0"
           }
       }
   },
   "require-feature": {
       "vendor/a": ["saas-provider"]
   }
}

Then i will have symfony/http-client and symfony/serializer in my composer.lock

@joelwurtz
Copy link
Contributor Author

joelwurtz commented Apr 4, 2025

(except that probably the lock file needs a list of features to be able to dump this in InstalledVersions style fashion if we want that.. Although I'd argue class_exists() checks should maybe be preferred at runtime to see if the dependency you need is present.

It's already done in this PR, i mainly have save the require-feature and feature field in the packages of the composer.lock so it's able to dump the InstalledVersions without update and there is a new method InstalledVersions::hasFeature('vendor/A', 'saas-provider') that allow to do those checks at runtime.

I think it's important to have that and not rely on class_exists, like in my example the feature may tell the user that some data will be fetched for an external site, so it require some dependencies to do that, however those libraries may be already present because they were required by another package but the user don't want to use this feature, with class_exists it is impossible to know the correct intent. (In fact it is possible but it's up to the library to propose a way to disable / use it, with this method it avoid that for simple case)

@joelwurtz joelwurtz force-pushed the feat/feature-system branch from 8c895a2 to c7d96c4 Compare May 5, 2025 07:24
@joelwurtz joelwurtz force-pushed the feat/feature-system branch 3 times, most recently from faea253 to 2203174 Compare May 23, 2025 15:27
@joelwurtz
Copy link
Contributor Author

Hey there,

Is there any news concerning looking at this ? I really believe this would be a great feature for a lot of libraries and maintainers and simplify a lot some processes.

From my point of view this PR is ready for a first version, but i'm available if you think it need more (or even less).

@Seldaek Seldaek added this to the 2.10 milestone Oct 30, 2025
@Seldaek
Copy link
Member

Seldaek commented Oct 30, 2025

I'm currently trying to wrap up 2.9, then I think I probably should look at this.. Sorry it's been so long. It's too big to sneak into 2.9 on a short schedule tho especially as it'll require packagist adjustments too, so I rather take time and let people play with snapshots for a while.

@joelwurtz
Copy link
Contributor Author

No worries, if this is accepted i would gladly do the PR on packagist to make the changes, if needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants