[
  {
    "path": ".gitignore",
    "content": "/vendor/\ncomposer.lock\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2017-2019 Fabien Potencier\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is furnished\nto do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\"><a href=\"https://symfony.com\" target=\"_blank\">\n    <img src=\"https://symfony.com/logos/symfony_black_02.svg\">\n</a></p>\n\nGive thanks (in the form of a [GitHub ★ ](https://help.github.com/articles/about-stars/)) to your fellow PHP package maintainers (not limited to Symfony components)!\n\nInstall\n-------\n\nInstall this as any other (dev) Composer package:\n```sh\ncomposer require --dev symfony/thanks\n```\n\nYou can also install it once for all your local projects:\n```sh\ncomposer global require symfony/thanks\n```\n\nUsage\n-----\n\n```sh\ncomposer thanks\n```\n\nThis will find all of your Composer dependencies, find their github.com repository, and star their GitHub repositories. This was inspired by `cargo thanks`, which was inspired in part by Medium's clapping button as a way to show thanks for someone else's work you've found enjoyment in.\n\nIf you're wondering why did some dependencies get thanked and not others, the answer is that this plugin only supports github.com at the moment. Pull requests are welcome to add support for thanking packages hosted on other services.\n\nOriginal idea by Doug Tangren (softprops) 2017 for Rust (thanks!)\n\nImplemented by Nicolas Grekas (SensioLabs & Blackfire.io) 2017 for PHP.\n\nForwarding stars\n----------------\n\nPackage authors can *send* a star to another package that they would like to thank.\n\nIf you are a package author and want to thank another repository, you can add a `thanks` entry in the `extra` section of your `composer.json` file.\n\nFor example, `symfony/webpack-encore-pack` sends a star to `symfony/webpack-encore`:\n\n```json\n{\n    \"extra\": {\n        \"thanks\": {\n            \"name\": \"symfony/webpack-encore\",\n            \"url\": \"https://github.com/symfony/webpack-encore\"\n        }\n    }\n}\n```\n"
  },
  {
    "path": "composer.json",
    "content": "{\n    \"name\": \"symfony/thanks\",\n    \"description\": \"Encourages sending ⭐ and 💵 to fellow PHP package maintainers (not limited to Symfony components)!\",\n    \"type\": \"composer-plugin\",\n    \"license\": \"MIT\",\n    \"authors\": [\n        {\n            \"name\": \"Nicolas Grekas\",\n            \"email\": \"p@tchwork.com\"\n        }\n    ],\n    \"require\": {\n        \"php\": \">=8.1\",\n        \"composer-plugin-api\": \"^1.0|^2.0\"\n    },\n    \"autoload\": {\n        \"psr-4\": {\n            \"Symfony\\\\Thanks\\\\\": \"src\"\n        }\n    },\n    \"extra\": {\n        \"branch-alias\": {\n            \"dev-main\": \"1.4-dev\"\n        },\n        \"class\": \"Symfony\\\\Thanks\\\\Thanks\"\n    }\n}\n"
  },
  {
    "path": "src/Command/FundCommand.php",
    "content": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Symfony\\Thanks\\Command;\n\nuse Composer\\Command\\BaseCommand;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Input\\InputOption;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\nuse Symfony\\Thanks\\GitHubClient;\n\n/**\n * @author Nicolas Grekas <p@tchwork.com>\n */\nclass FundCommand extends BaseCommand\n{\n    private $star = '★ ';\n    private $love = '💖 ';\n    private $cash = '💵 ';\n\n    protected function configure(): void\n    {\n        if ('Hyper' === getenv('TERM_PROGRAM')) {\n            $this->star = '⭐ ';\n        } elseif ('\\\\' === \\DIRECTORY_SEPARATOR) {\n            $this->star = '*';\n            $this->love = '<3';\n            $this->cash = '$$$';\n        }\n\n        $this->setName('fund')\n            ->setDescription(sprintf('Discover the funding links that fellow PHP package maintainers publish %s.', $this->cash))\n        ;\n    }\n\n    protected function execute(InputInterface $input, OutputInterface $output): int\n    {\n        $composer = $this->getComposer();\n        $gitHub = new GitHubClient($composer, $this->getIO());\n\n        $repos = $gitHub->getRepositories($failures, true);\n        $fundings = [];\n        $notStarred = 0;\n\n        foreach ($repos as $alias => $repo) {\n            $notStarred += (int) !$repo['viewerHasStarred'];\n\n            foreach ($repo['fundingLinks'] as $link) {\n                list($owner, $package) = explode('/', $repo['package'], 2);\n                $fundings[$owner][$link['url']][] = $package;\n            }\n        }\n\n        if ($fundings) {\n            $prev = null;\n\n            $output->writeln('The following packages were found in your dependencies and publish sponsoring links on their GitHub page:');\n\n            foreach ($fundings as $owner => $links) {\n                $output->writeln(sprintf(\"\\n<comment>%s</comment>\", $owner));\n                foreach ($links as $url => $packages) {\n                    $line = sprintf('  <info>%s/%s</>', $owner, implode(', ', $packages));\n\n                    if ($prev !== $line) {\n                        $output->writeln($line);\n                        $prev = $line;\n                    }\n                    $output->writeln(sprintf('    %s %s', $this->cash, $url));\n                }\n            }\n\n            $output->writeln(\"\\nPlease consider following these links and sponsoring the work of package authors!\");\n            $output->writeln(sprintf(\"\\nThank you! %s\", $this->love));\n        } else {\n            $output->writeln(\"No funding links were found in your package dependencies. This doesn't mean they don't need your support!\");\n        }\n\n        if ($notStarred) {\n            $output->writeln(sprintf(\"\\nRun <comment>composer thanks</> to send a %s to <comment>%d</comment> GitHub repositor%s of your fellow package maintainers.\", $this->star, $notStarred, 1 < $notStarred ? 'ies' : 'y'));\n        }\n\n        return 0;\n    }\n}\n"
  },
  {
    "path": "src/Command/ThanksCommand.php",
    "content": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Symfony\\Thanks\\Command;\n\nuse Composer\\Command\\BaseCommand;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Input\\InputOption;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\nuse Symfony\\Thanks\\GitHubClient;\n\n/**\n * @author Nicolas Grekas <p@tchwork.com>\n */\nclass ThanksCommand extends BaseCommand\n{\n    private $star = '★ ';\n    private $love = '💖 ';\n    private $cash = '💵 ';\n\n    protected function configure(): void\n    {\n        if ('Hyper' === getenv('TERM_PROGRAM')) {\n            $this->star = '⭐ ';\n        } elseif ('\\\\' === \\DIRECTORY_SEPARATOR) {\n            $this->star = '*';\n            $this->love = '<3';\n            $this->cash = '$$$';\n        }\n\n        $this->setName('thanks')\n            ->setDescription(sprintf('Give thanks (in the form of a GitHub %s) to your fellow PHP package maintainers.', $this->star))\n            ->setDefinition([\n                new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Don\\'t actually send the stars'),\n            ])\n        ;\n    }\n\n    protected function execute(InputInterface $input, OutputInterface $output): int\n    {\n        $composer = $this->getComposer();\n        $gitHub = new GitHubClient($composer, $this->getIO());\n\n        $repos = $gitHub->getRepositories($failures);\n\n        $template = '%1$s: addStar(input:{clientMutationId:\"%s\",starrableId:\"%s\"}){clientMutationId}'.\"\\n\";\n        $graphql = '';\n        $notStarred = [];\n\n        foreach ($repos as $alias => $repo) {\n            if (!$repo['viewerHasStarred']) {\n                $graphql .= sprintf($template, $alias, $repo['id']);\n                $notStarred[$alias] = $repo;\n            }\n        }\n\n        if (!$notStarred) {\n            $output->writeln('You already starred all your GitHub dependencies.');\n        } else {\n            if (!$input->getOption('dry-run')) {\n                $notStarred = $gitHub->call(sprintf(\"mutation{\\n%s}\", $graphql));\n            }\n\n            $output->writeln('Stars <comment>sent</> to:');\n            foreach ($repos as $alias => $repo) {\n                $output->writeln(sprintf(' %s %s - %s', $this->star, sprintf(isset($notStarred[$alias]) ? '<comment>%s</>' : '%s', $repo['package']), $repo['url']));\n            }\n        }\n\n        if ($failures) {\n            $output->writeln('');\n            $output->writeln('Some repositories could not be starred, please run <info>composer update</info> and try again:');\n\n            foreach ($failures as $alias => $failure) {\n                foreach ((array) $failure['messages'] as $message) {\n                    $output->writeln(sprintf(' * %s - %s', $failure['url'], $message));\n                }\n            }\n        }\n\n        $output->writeln(\"\\nPlease consider contributing back in any way if you can!\");\n        $output->writeln(sprintf(\"\\nRun <comment>composer fund</> to discover how you can sponsor your fellow PHP package maintainers %s\", $this->cash));\n        $output->writeln(sprintf(\"\\nThank you! %s\", $this->love));\n\n        return 0;\n    }\n}\n"
  },
  {
    "path": "src/GitHubClient.php",
    "content": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Symfony\\Thanks;\n\nuse Composer\\Composer;\nuse Composer\\Downloader\\TransportException;\nuse Composer\\Factory;\nuse Composer\\IO\\IOInterface;\nuse Composer\\Json\\JsonFile;\nuse Composer\\Util\\HttpDownloader;\n\n/**\n * @author Nicolas Grekas <p@tchwork.com>\n */\nclass GitHubClient\n{\n    // This is a list of projects that should get a star on their main repository\n    // (when there is one) whenever you use any of their other repositories.\n    // When a project's main repo is also a dependency of their other repos (like amphp/amp),\n    // there is no need to list it here, as starring will transitively happen anyway.\n    private static $mainRepositories = [\n        'api-platform' => [\n            'name' => 'api-platform/api-platform',\n            'url' => 'https://github.com/api-platform/api-platform',\n        ],\n        'cakephp' => [\n            'name' => 'cakephp/cakephp',\n            'url' => 'https://github.com/cakephp/cakephp',\n        ],\n        'drupal' => [\n            'name' => 'drupal/drupal',\n            'url' => 'https://github.com/drupal/drupal',\n        ],\n        'laravel' => [\n            'name' => 'laravel/laravel',\n            'url' => 'https://github.com/laravel/laravel',\n        ],\n        'illuminate' => [\n            'name' => 'laravel/laravel',\n            'url' => 'https://github.com/laravel/laravel',\n        ],\n        'nette' => [\n            'name' => 'nette/nette',\n            'url' => 'https://github.com/nette/nette',\n        ],\n        'phpDocumentor' => [\n            'name' => 'phpDocumentor/phpDocumentor2',\n            'url' => 'https://github.com/phpDocumentor/phpDocumentor2',\n        ],\n        'matomo' => [\n            'name' => 'piwik/piwik',\n            'url' => 'https://github.com/matomo-org/matomo',\n        ],\n        'reactphp' => [\n            'name' => 'reactphp/react',\n            'url' => 'https://github.com/reactphp/react',\n        ],\n        'sebastianbergmann' => [\n            'name' => 'phpunit/phpunit',\n            'url' => 'https://github.com/sebastianbergmann/phpunit',\n        ],\n        'slimphp' => [\n            'name' => 'slimphp/Slim',\n            'url' => 'https://github.com/slimphp/Slim',\n        ],\n        'Sylius' => [\n            'name' => 'Sylius/Sylius',\n            'url' => 'https://github.com/Sylius/Sylius',\n        ],\n        'symfony' => [\n            'name' => 'symfony/symfony',\n            'url' => 'https://github.com/symfony/symfony',\n        ],\n        'yiisoft' => [\n            'name' => 'yiisoft/yii2',\n            'url' => 'https://github.com/yiisoft/yii2',\n        ],\n        'zendframework' => [\n            'name' => 'zendframework/zendframework',\n            'url' => 'https://github.com/zendframework/zendframework',\n        ],\n    ];\n\n    private $composer;\n    private $io;\n    private $rfs;\n\n    public function __construct(Composer $composer, IOInterface $io)\n    {\n        $this->composer = $composer;\n        $this->io = $io;\n\n        if (class_exists(HttpDownloader::class)) {\n            $this->rfs = new HttpDownloader($io, $composer->getConfig());\n        } else {\n            $this->rfs = Factory::createRemoteFilesystem($io, $composer->getConfig());\n        }\n    }\n\n    public function getRepositories(?array &$failures = null, bool $withFundingLinks = false): array\n    {\n        $repo = $this->composer->getRepositoryManager()->getLocalRepository();\n\n        $urls = [\n            'composer/composer' => 'https://github.com/composer/composer',\n            'php/php-src' => 'https://github.com/php/php-src',\n            'symfony/thanks' => 'https://github.com/symfony/thanks',\n        ];\n\n        $directPackages = $this->getDirectlyRequiredPackageNames();\n        // symfony/thanks shouldn't trigger thanking symfony/symfony\n        unset($directPackages['symfony/thanks']);\n        foreach ($repo->getPackages() as $package) {\n            $extra = $package->getExtra();\n\n            if (isset($extra['thanks']['name'], $extra['thanks']['url'])) {\n                $urls += [$extra['thanks']['name'] => $extra['thanks']['url']];\n            }\n\n            if (!$url = $package->getSourceUrl()) {\n                continue;\n            }\n\n            $urls[$package->getName()] = $url;\n\n            if (!preg_match('#^https://github.com/([^/]++)#', $url, $url)) {\n                continue;\n            }\n            $owner = $url[1];\n\n            // star the main repository, but only if this package is directly\n            // being required by the user's composer.json\n            if (isset(self::$mainRepositories[$owner], $directPackages[$package->getName()])) {\n                $urls[self::$mainRepositories[$owner]['name']] = self::$mainRepositories[$owner]['url'];\n            }\n        }\n\n        ksort($urls);\n\n        $chunks = array_chunk($urls, 150, true);\n\n        $repos = [];\n\n        foreach ($chunks as $chunk) {\n            $repos += $this->processChunks($chunk, $withFundingLinks, $failures);\n        }\n\n        return $repos;\n    }\n\n    public function call($graphql, array &$failures = []): mixed\n    {\n        $options = [\n            'http' => [\n                'method' => 'POST',\n                'content' => json_encode(['query' => $graphql]),\n                'header' => ['Content-Type: application/json'],\n            ],\n        ];\n\n        if ($this->rfs instanceof HttpDownloader) {\n            $result = $this->rfs->get('https://api.github.com/graphql', $options)->getBody();\n        } else {\n            $result = $this->rfs->getContents('github.com', 'https://api.github.com/graphql', false, $options);\n        }\n\n        $result = json_decode($result, true);\n\n        if (isset($result['errors'][0]['message'])) {\n            if (!isset($result['data'])) {\n                throw new TransportException($result['errors'][0]['message']);\n            }\n\n            foreach ($result['errors'] as $error) {\n                if (!isset($error['path'])) {\n                    $failures[isset($error['type']) ? $error['type'] : $error['message']] = $error['message'];\n                    continue;\n                }\n\n                foreach ($error['path'] as $path) {\n                    $failures += [$path => $error['message']];\n                    unset($result['data'][$path]);\n                }\n            }\n        }\n\n        return isset($result['data']) ? $result['data'] : [];\n    }\n\n    private function getDirectlyRequiredPackageNames(): array\n    {\n        $file = new JsonFile(Factory::getComposerFile(), null, $this->io);\n\n        if (!$file->exists()) {\n            throw new \\Exception('Could not find your composer.json file!');\n        }\n\n        $data = $file->read() + ['require' => [], 'require-dev' => []];\n        $data = array_keys($data['require'] + $data['require-dev']);\n\n        return array_combine($data, $data);\n    }\n\n    private function processChunks(array $urls, bool $withFundingLinks, ?array &$failures = null): array\n    {\n        $i = 0;\n        $template = $withFundingLinks\n            ? '_%d: repository(owner:\"%s\",name:\"%s\"){id,viewerHasStarred,fundingLinks{platform,url}}'.\"\\n\"\n            : '_%d: repository(owner:\"%s\",name:\"%s\"){id,viewerHasStarred}'.\"\\n\";\n        $graphql = '';\n\n        $aliases = [];\n\n        foreach ($urls as $package => $url) {\n            if (preg_match('#^https://github.com/([^/]++)/(.*?)(?:\\.git)?$#i', $url, $url)) {\n                $graphql .= sprintf($template, ++$i, $url[1], $url[2]);\n                $aliases['_'.$i] = [$package, sprintf('https://github.com/%s/%s', $url[1], $url[2])];\n            }\n        }\n\n        $failures = [];\n        $repos = [];\n\n        foreach ($this->call(sprintf(\"query{\\n%s}\", $graphql), $failures) as $alias => $repo) {\n            $repo['package'] = $aliases[$alias][0];\n            $repo['url'] = $aliases[$alias][1];\n            $repos[$alias] = $repo;\n        }\n\n        foreach ($failures as $alias => $messages) {\n            $failures[$alias] = [\n                'messages' => $messages,\n                'package' => $aliases[$alias][0],\n                'url' => $aliases[$alias][1],\n            ];\n        }\n\n        return $repos;\n    }\n}\n"
  },
  {
    "path": "src/Thanks.php",
    "content": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Symfony\\Thanks;\n\nuse Composer\\Composer;\nuse Composer\\Console\\Application;\nuse Composer\\EventDispatcher\\EventSubscriberInterface;\nuse Composer\\Installer\\PackageEvents;\nuse Composer\\IO\\IOInterface;\nuse Composer\\Plugin\\PluginInterface;\nuse Composer\\Script\\Event as ScriptEvent;\nuse Composer\\Script\\ScriptEvents;\nuse Symfony\\Component\\Console\\Input\\ArgvInput;\n\n/**\n * @author Nicolas Grekas <p@tchwork.com>\n */\nclass Thanks implements EventSubscriberInterface, PluginInterface\n{\n    private $io;\n    private $displayReminder = 0;\n\n    public function activate(Composer $composer, IOInterface $io): void\n    {\n        $this->io = $io;\n        $addCommand = 'add'.(method_exists(Application::class, 'addCommand') ? 'Command' : '');\n\n        foreach (debug_backtrace() as $trace) {\n            if (!isset($trace['object']) || !isset($trace['args'][0])) {\n                continue;\n            }\n\n            if (!$trace['object'] instanceof Application || !$trace['args'][0] instanceof ArgvInput) {\n                continue;\n            }\n\n            $input = $trace['args'][0];\n            $app = $trace['object'];\n\n            try {\n                $command = $input->getFirstArgument();\n                $command = $command ? $app->find($command)->getName() : null;\n            } catch (\\InvalidArgumentException $e) {\n            }\n\n            if ('update' === $command) {\n                $this->displayReminder = 1;\n            }\n\n            $app->$addCommand(new Command\\ThanksCommand());\n\n            if (!$app->has('fund')) {\n                $app->$addCommand(new Command\\FundCommand());\n            }\n\n            break;\n        }\n    }\n\n    public function enableReminder(): void\n    {\n        if (1 === $this->displayReminder) {\n            $this->displayReminder = version_compare('1.1.0', PluginInterface::PLUGIN_API_VERSION, '<=') ? 2 : 0;\n        }\n    }\n\n    public function displayReminder(ScriptEvent $event): void\n    {\n        if (2 !== $this->displayReminder) {\n            return;\n        }\n\n        $gitHub = new GitHubClient($event->getComposer(), $event->getIO());\n\n        $notStarred = 0;\n        foreach ($gitHub->getRepositories() as $repo) {\n            $notStarred += (int) !$repo['viewerHasStarred'];\n        }\n\n        if (!$notStarred) {\n            return;\n        }\n\n        $love = '💖 ';\n        $star = '★ ';\n        $cash = '💵 ';\n\n        if ('Hyper' === getenv('TERM_PROGRAM')) {\n            $star = '⭐ ';\n        } elseif ('\\\\' === \\DIRECTORY_SEPARATOR) {\n            $love = 'love';\n            $star = 'star';\n            $cash = 'cash.';\n        }\n\n        $this->io->writeError('');\n        $this->io->writeError('What about running <comment>composer thanks</> now?</>');\n        $this->io->writeError(sprintf('This will spread some %s by sending a %s to <comment>%d</comment> GitHub repositor%s of your fellow package maintainers.', $love, $star, $notStarred, 1 < $notStarred ? 'ies' : 'y'));\n        $this->io->writeError(sprintf('You can also run <comment>composer fund</> to discover how you can sponsor their work with some %s</>', $cash));\n        $this->io->writeError('');\n    }\n\n    public static function getSubscribedEvents(): array\n    {\n        return [\n            PackageEvents::POST_PACKAGE_UPDATE => 'enableReminder',\n            ScriptEvents::POST_UPDATE_CMD => 'displayReminder',\n        ];\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function deactivate(Composer $composer, IOInterface $io): void\n    {\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function uninstall(Composer $composer, IOInterface $io): void\n    {\n    }\n}\n"
  }
]