Merge pull request #8453 from naderman/funding
Add funding field to composer.json and composer fund commandpull/8611/head
commit
055a179cc5
|
@ -420,6 +420,11 @@ This implies `--by-package --by-suggestion`, showing both lists.
|
|||
* **--by-suggestion:** Groups output by suggested package.
|
||||
* **--no-dev:** Excludes suggestions from `require-dev` packages.
|
||||
|
||||
## fund
|
||||
|
||||
Discover how to help fund the maintenance of your dependencies. This lists
|
||||
all funding links from the installed dependencies.
|
||||
|
||||
## depends (why)
|
||||
|
||||
The `depends` command tells you which other packages depend on a certain
|
||||
|
|
|
@ -258,6 +258,39 @@ An example:
|
|||
|
||||
Optional.
|
||||
|
||||
### funding
|
||||
|
||||
A list of URLs to provide funding to the package authors for maintenance and
|
||||
development of new functionality.
|
||||
|
||||
Each entry consists of the following
|
||||
|
||||
* **type:** The type of funding or the platform through which funding can be provided, e.g. patreon, opencollective, tidelift or github.
|
||||
* **url:** URL to a website with details and a way to fund the package.
|
||||
|
||||
An example:
|
||||
|
||||
```json
|
||||
{
|
||||
"funding": [
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/phpdoctrine"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/subscription/pkg/packagist-doctrine_doctrine-bundle"
|
||||
},
|
||||
{
|
||||
"type": "other",
|
||||
"url": "https://www.doctrine-project.org/sponsorship.html"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Optional.
|
||||
|
||||
### Package links
|
||||
|
||||
All of the following take an object which maps package names to
|
||||
|
|
|
@ -522,6 +522,24 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"funding": {
|
||||
"type": "array",
|
||||
"description": "A list of options to fund the development and maintenance of the package.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Type of funding or platform through which funding is possible."
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "URL to a website with details on funding and a way to fund the package.",
|
||||
"format": "uri"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"non-feature-branches": {
|
||||
"type": ["array"],
|
||||
"description": "A set of string or regex patterns for non-numeric branch names that will not be handled as feature branches.",
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Composer.
|
||||
*
|
||||
* (c) Nils Adermann <naderman@naderman.de>
|
||||
* Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer\Command;
|
||||
|
||||
use Composer\Package\CompletePackageInterface;
|
||||
use Composer\Package\AliasPackage;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
* @author Jordi Boggiano <j.boggiano@seld.be>
|
||||
*/
|
||||
class FundCommand extends BaseCommand
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('fund')
|
||||
->setDescription('Discover how to help fund the maintenance of your dependencies.')
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$composer = $this->getComposer();
|
||||
|
||||
$repo = $composer->getRepositoryManager()->getLocalRepository();
|
||||
$fundings = array();
|
||||
foreach ($repo->getPackages() as $package) {
|
||||
if ($package instanceof AliasPackage) {
|
||||
continue;
|
||||
}
|
||||
if ($package instanceof CompletePackageInterface && $funding = $package->getFunding()) {
|
||||
foreach ($funding as $fundingOption) {
|
||||
list($vendor, $packageName) = explode('/', $package->getPrettyName());
|
||||
$url = $fundingOption['url'];
|
||||
if (!empty($fundingOption['type']) && $fundingOption['type'] === 'github' && preg_match('{^https://github.com/([^/]+)$}', $url, $match)) {
|
||||
$url = 'https://github.com/sponsors/'.$match[1];
|
||||
}
|
||||
$fundings[$vendor][$url][] = $packageName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ksort($fundings);
|
||||
|
||||
$io = $this->getIO();
|
||||
|
||||
if ($fundings) {
|
||||
$prev = null;
|
||||
|
||||
$io->write('The following packages were found in your dependencies which publish funding information:');
|
||||
|
||||
foreach ($fundings as $vendor => $links) {
|
||||
$io->write('');
|
||||
$io->write(sprintf("<comment>%s</comment>", $vendor));
|
||||
foreach ($links as $url => $packages) {
|
||||
$line = sprintf(' <info>%s</info>', implode(', ', $packages));
|
||||
|
||||
if ($prev !== $line) {
|
||||
$io->write($line);
|
||||
$prev = $line;
|
||||
}
|
||||
|
||||
$io->write(sprintf(' %s', $url));
|
||||
}
|
||||
}
|
||||
|
||||
$io->write("");
|
||||
$io->write("Please consider following these links and sponsoring the work of package authors!");
|
||||
$io->write("Thank you!");
|
||||
} else {
|
||||
$io->write("No funding links were found in your package dependencies. This doesn't mean they don't need your support!");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
|
@ -436,6 +436,7 @@ class Application extends BaseApplication
|
|||
new Command\ExecCommand(),
|
||||
new Command\OutdatedCommand(),
|
||||
new Command\CheckPlatformReqsCommand(),
|
||||
new Command\FundCommand(),
|
||||
));
|
||||
|
||||
if ('phar:' === substr(__FILE__, 0, 5)) {
|
||||
|
|
|
@ -35,6 +35,7 @@ use Composer\IO\IOInterface;
|
|||
use Composer\Package\AliasPackage;
|
||||
use Composer\Package\BasePackage;
|
||||
use Composer\Package\CompletePackage;
|
||||
use Composer\Package\CompletePackageInterface;
|
||||
use Composer\Package\Link;
|
||||
use Composer\Package\Loader\ArrayLoader;
|
||||
use Composer\Package\Dumper\ArrayDumper;
|
||||
|
@ -313,6 +314,24 @@ class Installer
|
|||
}
|
||||
}
|
||||
|
||||
$fundingCount = 0;
|
||||
foreach ($localRepo->getPackages() as $package) {
|
||||
if ($package instanceof CompletePackageInterface && !$package instanceof AliasPackage && $package->getFunding()) {
|
||||
$fundingCount++;
|
||||
}
|
||||
}
|
||||
if ($fundingCount) {
|
||||
$this->io->writeError(array(
|
||||
sprintf(
|
||||
"<info>%d package%s you are using %s looking for funding.</info>",
|
||||
$fundingCount,
|
||||
1 === $fundingCount ? '' : 's',
|
||||
1 === $fundingCount ? 'is' : 'are'
|
||||
),
|
||||
'<info>Use the composer fund command to find out more!</info>',
|
||||
));
|
||||
}
|
||||
|
||||
if ($this->runScripts) {
|
||||
// dispatch post event
|
||||
$eventName = $this->update ? ScriptEvents::POST_UPDATE_CMD : ScriptEvents::POST_INSTALL_CMD;
|
||||
|
|
|
@ -377,6 +377,11 @@ class AliasPackage extends BasePackage implements CompletePackageInterface
|
|||
return $this->aliasOf->getSupport();
|
||||
}
|
||||
|
||||
public function getFunding()
|
||||
{
|
||||
return $this->aliasOf->getFunding();
|
||||
}
|
||||
|
||||
public function getNotificationUrl()
|
||||
{
|
||||
return $this->aliasOf->getNotificationUrl();
|
||||
|
|
|
@ -27,6 +27,7 @@ class CompletePackage extends Package implements CompletePackageInterface
|
|||
protected $homepage;
|
||||
protected $scripts = array();
|
||||
protected $support = array();
|
||||
protected $funding = array();
|
||||
protected $abandoned = false;
|
||||
|
||||
/**
|
||||
|
@ -171,6 +172,24 @@ class CompletePackage extends Package implements CompletePackageInterface
|
|||
return $this->support;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the funding
|
||||
*
|
||||
* @param array $funding
|
||||
*/
|
||||
public function setFunding(array $funding)
|
||||
{
|
||||
$this->funding = $funding;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getFunding()
|
||||
{
|
||||
return $this->funding;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
|
|
|
@ -79,6 +79,15 @@ interface CompletePackageInterface extends PackageInterface
|
|||
*/
|
||||
public function getSupport();
|
||||
|
||||
/**
|
||||
* Returns an array of funding options for the package
|
||||
*
|
||||
* Each item will contain type and url keys
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFunding();
|
||||
|
||||
/**
|
||||
* Returns if the package is abandoned or not
|
||||
*
|
||||
|
|
|
@ -104,6 +104,7 @@ class ArrayDumper
|
|||
'keywords',
|
||||
'repositories',
|
||||
'support',
|
||||
'funding',
|
||||
);
|
||||
|
||||
$data = $this->dumpValues($package, $keys, $data);
|
||||
|
|
|
@ -198,6 +198,10 @@ class ArrayLoader implements LoaderInterface
|
|||
$package->setSupport($config['support']);
|
||||
}
|
||||
|
||||
if (!empty($config['funding']) && is_array($config['funding'])) {
|
||||
$package->setFunding($config['funding']);
|
||||
}
|
||||
|
||||
if (isset($config['abandoned'])) {
|
||||
$package->setAbandoned($config['abandoned']);
|
||||
}
|
||||
|
|
|
@ -193,6 +193,32 @@ class ValidatingArrayLoader implements LoaderInterface
|
|||
}
|
||||
}
|
||||
|
||||
if ($this->validateArray('funding') && !empty($this->config['funding'])) {
|
||||
foreach ($this->config['funding'] as $key => $fundingOption) {
|
||||
if (!is_array($fundingOption)) {
|
||||
$this->errors[] = 'funding.'.$key.' : should be an array, '.gettype($fundingOption).' given';
|
||||
unset($this->config['funding'][$key]);
|
||||
continue;
|
||||
}
|
||||
foreach (array('type', 'url') as $fundingData) {
|
||||
if (isset($fundingOption[$fundingData]) && !is_string($fundingOption[$fundingData])) {
|
||||
$this->errors[] = 'funding.'.$key.'.'.$fundingData.' : invalid value, must be a string';
|
||||
unset($this->config['funding'][$key][$fundingData]);
|
||||
}
|
||||
}
|
||||
if (isset($fundingOption['url']) && !$this->filterUrl($fundingOption['url'])) {
|
||||
$this->warnings[] = 'funding.'.$key.'.url : invalid value ('.$fundingOption['url'].'), must be an http/https URL';
|
||||
unset($this->config['funding'][$key]['url']);
|
||||
}
|
||||
if (empty($this->config['funding'][$key])) {
|
||||
unset($this->config['funding'][$key]);
|
||||
}
|
||||
}
|
||||
if (empty($this->config['funding'])) {
|
||||
unset($this->config['funding']);
|
||||
}
|
||||
}
|
||||
|
||||
$unboundConstraint = new Constraint('=', $this->versionParser->normalize('dev-master'));
|
||||
$stableConstraint = new Constraint('=', '1.0.0');
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@ class GitHubDriver extends VcsDriver
|
|||
protected $infoCache = array();
|
||||
protected $isPrivate = false;
|
||||
private $isArchived = false;
|
||||
private $fundingInfo;
|
||||
|
||||
/**
|
||||
* Git Driver
|
||||
|
@ -166,6 +167,10 @@ class GitHubDriver extends VcsDriver
|
|||
if (!isset($composer['abandoned']) && $this->isArchived) {
|
||||
$composer['abandoned'] = true;
|
||||
}
|
||||
|
||||
if (!isset($composer['funding']) && $funding = $this->getFundingInfo()) {
|
||||
$composer['funding'] = $funding;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->shouldCache($identifier)) {
|
||||
|
@ -178,6 +183,40 @@ class GitHubDriver extends VcsDriver
|
|||
return $this->infoCache[$identifier];
|
||||
}
|
||||
|
||||
private function getFundingInfo()
|
||||
{
|
||||
if (null !== $this->fundingInfo) {
|
||||
return $this->fundingInfo;
|
||||
}
|
||||
|
||||
if ($this->originUrl !== 'github.com') {
|
||||
return $this->fundingInfo = false;
|
||||
}
|
||||
|
||||
$graphql = 'query{repository(owner:"'.$this->owner.'",name:"'.$this->repository.'"){fundingLinks{platform,url}}}';
|
||||
try {
|
||||
$result = $this->remoteFilesystem->getContents($this->originUrl, 'https://api.github.com/graphql', false, array(
|
||||
'http' => array(
|
||||
'method' => 'POST',
|
||||
'content' => json_encode(array('query' => $graphql)),
|
||||
'header' => array('Content-Type: application/json'),
|
||||
),
|
||||
'retry-auth-failure' => false,
|
||||
));
|
||||
} catch (\TransportException $e) {
|
||||
return $this->fundingInfo = false;
|
||||
}
|
||||
$result = json_decode($result, true);
|
||||
|
||||
if (empty($result['data']['repository']['fundingLinks'])) {
|
||||
return $this->fundingInfo = false;
|
||||
}
|
||||
|
||||
return $this->fundingInfo = array_map(function ($link) {
|
||||
return array('type' => strtolower($link['platform']), 'url' => $link['url']);
|
||||
}, $result['data']['repository']['fundingLinks']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
--TEST--
|
||||
Installs a simple package with exact match requirement
|
||||
--COMPOSER--
|
||||
{
|
||||
"repositories": [
|
||||
{
|
||||
"type": "package",
|
||||
"package": [
|
||||
{
|
||||
"name": "a/a",
|
||||
"version": "1.0.0",
|
||||
"funding": [{ "type": "example", "url": "http://example.org/fund" }],
|
||||
"require": {
|
||||
"d/d": "^1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "b/b",
|
||||
"version": "1.0.0",
|
||||
"funding": [{ "type": "example", "url": "http://example.org/fund" }]
|
||||
},
|
||||
{
|
||||
"name": "c/c",
|
||||
"version": "1.0.0",
|
||||
"funding": [{ "type": "example", "url": "http://example.org/fund" }]
|
||||
},
|
||||
{
|
||||
"name": "d/d",
|
||||
"version": "1.0.0",
|
||||
"require": {
|
||||
"b/b": "^1.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"a/a": "1.0.0"
|
||||
}
|
||||
}
|
||||
--RUN--
|
||||
install
|
||||
--EXPECT-OUTPUT--
|
||||
Loading composer repositories with package information
|
||||
Updating dependencies (including require-dev)
|
||||
Package operations: 3 installs, 0 updates, 0 removals
|
||||
Writing lock file
|
||||
Generating autoload files
|
||||
2 packages you are using are looking for funding.
|
||||
Use the composer fund command to find out more!
|
||||
--EXPECT--
|
||||
Installing b/b (1.0.0)
|
||||
Installing d/d (1.0.0)
|
||||
Installing a/a (1.0.0)
|
|
@ -21,6 +21,12 @@
|
|||
"irc": "irc://irc.freenode.org/composer",
|
||||
"issues": "https://github.com/composer/composer/issues"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"type": "service-subscription",
|
||||
"url": "https://packagist.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.3.2",
|
||||
"justinrainbow/json-schema": "~1.4",
|
||||
|
|
|
@ -191,6 +191,10 @@ class ArrayDumperTest extends TestCase
|
|||
'support',
|
||||
array('foo' => 'bar'),
|
||||
),
|
||||
array(
|
||||
'funding',
|
||||
array('type' => 'foo', 'url' => 'https://example.com'),
|
||||
),
|
||||
array(
|
||||
'require',
|
||||
array(new Link('foo', 'foo/bar', new Constraint('=', '1.0.0.0'), 'requires', '1.0.0'), new Link('bar', 'bar/baz', new Constraint('=', '1.0.0.0'), 'requires', '1.0.0')),
|
||||
|
|
|
@ -97,6 +97,9 @@ class ArrayLoaderTest extends TestCase
|
|||
'authors' => array(
|
||||
array('name' => 'Bob', 'email' => 'bob@example.org', 'homepage' => 'example.org', 'role' => 'Developer'),
|
||||
),
|
||||
'funding' => array(
|
||||
array('type' => 'example', 'url' => 'https://example.org/fund'),
|
||||
),
|
||||
'require' => array(
|
||||
'foo/bar' => '1.0',
|
||||
),
|
||||
|
|
|
@ -73,6 +73,15 @@ class ValidatingArrayLoaderTest extends TestCase
|
|||
'rss' => 'http://example.org/rss',
|
||||
'chat' => 'http://example.org/chat',
|
||||
),
|
||||
'funding' => array(
|
||||
array(
|
||||
'type' => 'example',
|
||||
'url' => 'https://example.org/fund'
|
||||
),
|
||||
array(
|
||||
'url' => 'https://example.org/fund'
|
||||
),
|
||||
),
|
||||
'require' => array(
|
||||
'a/b' => '1.*',
|
||||
'b/c' => '~2',
|
||||
|
|
Loading…
Reference in New Issue