1
0
Fork 0

Merge pull request #8453 from naderman/funding

Add funding field to composer.json and composer fund command
pull/8611/head
Jordi Boggiano 2020-02-14 14:27:12 +01:00 committed by GitHub
commit 055a179cc5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 344 additions and 0 deletions

View File

@ -420,6 +420,11 @@ This implies `--by-package --by-suggestion`, showing both lists.
* **--by-suggestion:** Groups output by suggested package. * **--by-suggestion:** Groups output by suggested package.
* **--no-dev:** Excludes suggestions from `require-dev` packages. * **--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) ## depends (why)
The `depends` command tells you which other packages depend on a certain The `depends` command tells you which other packages depend on a certain

View File

@ -258,6 +258,39 @@ An example:
Optional. 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 ### Package links
All of the following take an object which maps package names to All of the following take an object which maps package names to

View File

@ -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": { "non-feature-branches": {
"type": ["array"], "type": ["array"],
"description": "A set of string or regex patterns for non-numeric branch names that will not be handled as feature branches.", "description": "A set of string or regex patterns for non-numeric branch names that will not be handled as feature branches.",

View File

@ -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;
}
}

View File

@ -436,6 +436,7 @@ class Application extends BaseApplication
new Command\ExecCommand(), new Command\ExecCommand(),
new Command\OutdatedCommand(), new Command\OutdatedCommand(),
new Command\CheckPlatformReqsCommand(), new Command\CheckPlatformReqsCommand(),
new Command\FundCommand(),
)); ));
if ('phar:' === substr(__FILE__, 0, 5)) { if ('phar:' === substr(__FILE__, 0, 5)) {

View File

@ -35,6 +35,7 @@ use Composer\IO\IOInterface;
use Composer\Package\AliasPackage; use Composer\Package\AliasPackage;
use Composer\Package\BasePackage; use Composer\Package\BasePackage;
use Composer\Package\CompletePackage; use Composer\Package\CompletePackage;
use Composer\Package\CompletePackageInterface;
use Composer\Package\Link; use Composer\Package\Link;
use Composer\Package\Loader\ArrayLoader; use Composer\Package\Loader\ArrayLoader;
use Composer\Package\Dumper\ArrayDumper; 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) { if ($this->runScripts) {
// dispatch post event // dispatch post event
$eventName = $this->update ? ScriptEvents::POST_UPDATE_CMD : ScriptEvents::POST_INSTALL_CMD; $eventName = $this->update ? ScriptEvents::POST_UPDATE_CMD : ScriptEvents::POST_INSTALL_CMD;

View File

@ -377,6 +377,11 @@ class AliasPackage extends BasePackage implements CompletePackageInterface
return $this->aliasOf->getSupport(); return $this->aliasOf->getSupport();
} }
public function getFunding()
{
return $this->aliasOf->getFunding();
}
public function getNotificationUrl() public function getNotificationUrl()
{ {
return $this->aliasOf->getNotificationUrl(); return $this->aliasOf->getNotificationUrl();

View File

@ -27,6 +27,7 @@ class CompletePackage extends Package implements CompletePackageInterface
protected $homepage; protected $homepage;
protected $scripts = array(); protected $scripts = array();
protected $support = array(); protected $support = array();
protected $funding = array();
protected $abandoned = false; protected $abandoned = false;
/** /**
@ -171,6 +172,24 @@ class CompletePackage extends Package implements CompletePackageInterface
return $this->support; 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 * @return bool
*/ */

View File

@ -79,6 +79,15 @@ interface CompletePackageInterface extends PackageInterface
*/ */
public function getSupport(); 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 * Returns if the package is abandoned or not
* *

View File

@ -104,6 +104,7 @@ class ArrayDumper
'keywords', 'keywords',
'repositories', 'repositories',
'support', 'support',
'funding',
); );
$data = $this->dumpValues($package, $keys, $data); $data = $this->dumpValues($package, $keys, $data);

View File

@ -198,6 +198,10 @@ class ArrayLoader implements LoaderInterface
$package->setSupport($config['support']); $package->setSupport($config['support']);
} }
if (!empty($config['funding']) && is_array($config['funding'])) {
$package->setFunding($config['funding']);
}
if (isset($config['abandoned'])) { if (isset($config['abandoned'])) {
$package->setAbandoned($config['abandoned']); $package->setAbandoned($config['abandoned']);
} }

View File

@ -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')); $unboundConstraint = new Constraint('=', $this->versionParser->normalize('dev-master'));
$stableConstraint = new Constraint('=', '1.0.0'); $stableConstraint = new Constraint('=', '1.0.0');

View File

@ -35,6 +35,7 @@ class GitHubDriver extends VcsDriver
protected $infoCache = array(); protected $infoCache = array();
protected $isPrivate = false; protected $isPrivate = false;
private $isArchived = false; private $isArchived = false;
private $fundingInfo;
/** /**
* Git Driver * Git Driver
@ -166,6 +167,10 @@ class GitHubDriver extends VcsDriver
if (!isset($composer['abandoned']) && $this->isArchived) { if (!isset($composer['abandoned']) && $this->isArchived) {
$composer['abandoned'] = true; $composer['abandoned'] = true;
} }
if (!isset($composer['funding']) && $funding = $this->getFundingInfo()) {
$composer['funding'] = $funding;
}
} }
if ($this->shouldCache($identifier)) { if ($this->shouldCache($identifier)) {
@ -178,6 +183,40 @@ class GitHubDriver extends VcsDriver
return $this->infoCache[$identifier]; 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} * {@inheritdoc}
*/ */

View File

@ -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)

View File

@ -21,6 +21,12 @@
"irc": "irc://irc.freenode.org/composer", "irc": "irc://irc.freenode.org/composer",
"issues": "https://github.com/composer/composer/issues" "issues": "https://github.com/composer/composer/issues"
}, },
"funding": [
{
"type": "service-subscription",
"url": "https://packagist.com"
}
],
"require": { "require": {
"php": ">=5.3.2", "php": ">=5.3.2",
"justinrainbow/json-schema": "~1.4", "justinrainbow/json-schema": "~1.4",

View File

@ -191,6 +191,10 @@ class ArrayDumperTest extends TestCase
'support', 'support',
array('foo' => 'bar'), array('foo' => 'bar'),
), ),
array(
'funding',
array('type' => 'foo', 'url' => 'https://example.com'),
),
array( array(
'require', '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')), 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')),

View File

@ -97,6 +97,9 @@ class ArrayLoaderTest extends TestCase
'authors' => array( 'authors' => array(
array('name' => 'Bob', 'email' => 'bob@example.org', 'homepage' => 'example.org', 'role' => 'Developer'), array('name' => 'Bob', 'email' => 'bob@example.org', 'homepage' => 'example.org', 'role' => 'Developer'),
), ),
'funding' => array(
array('type' => 'example', 'url' => 'https://example.org/fund'),
),
'require' => array( 'require' => array(
'foo/bar' => '1.0', 'foo/bar' => '1.0',
), ),

View File

@ -73,6 +73,15 @@ class ValidatingArrayLoaderTest extends TestCase
'rss' => 'http://example.org/rss', 'rss' => 'http://example.org/rss',
'chat' => 'http://example.org/chat', 'chat' => 'http://example.org/chat',
), ),
'funding' => array(
array(
'type' => 'example',
'url' => 'https://example.org/fund'
),
array(
'url' => 'https://example.org/fund'
),
),
'require' => array( 'require' => array(
'a/b' => '1.*', 'a/b' => '1.*',
'b/c' => '~2', 'b/c' => '~2',