├── .gitignore
├── tests
├── __snapshots__
│ ├── OutputTest__summary_for_5_packages__1.txt
│ ├── OutputTest__package_list_of_command__1.txt
│ └── OutputTest__single_package_with_default_teaser__1.txt
├── PluginTest.php
├── Pest.php
└── OutputTest.php
├── src
├── Command
│ ├── Provider.php
│ └── TreewareCommand.php
├── Output
│ ├── Summary.php
│ ├── PackageList.php
│ └── SinglePackage.php
├── PackageStatsClient.php
├── PackageRepo.php
├── Package.php
└── Plugin.php
├── phpunit.xml
├── composer.json
├── LICENSE
├── ecs.php
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | /composer.lock
3 | /coverage/
4 | /.phpunit.result.cache
5 |
--------------------------------------------------------------------------------
/tests/__snapshots__/OutputTest__summary_for_5_packages__1.txt:
--------------------------------------------------------------------------------
1 |
2 |
3 | 🌳 5 packages you are using with a Treeware licence
4 | 🌳 use the `composer treeware` command to find out more!
5 |
--------------------------------------------------------------------------------
/src/Command/Provider.php:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 | ./tests
10 |
11 |
12 |
13 |
14 | ./src
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/tests/PluginTest.php:
--------------------------------------------------------------------------------
1 | toEqual(['post-update-cmd' => 'showBanner']);
11 | });
12 |
13 | test('initialize composer command', function ()
14 | {
15 | $plugin = new \Treeware\Plant\Plugin();
16 | expect($plugin)->toBeInstanceOf(Capable::class);
17 | expect($plugin->getCapabilities())->toEqual([
18 | CommandProvider::class => Provider::class,
19 | ]);
20 | });
21 |
--------------------------------------------------------------------------------
/src/Output/Summary.php:
--------------------------------------------------------------------------------
1 | io = $io;
17 | }
18 |
19 | public function show(int $count): void
20 | {
21 | $this->io->write(PHP_EOL);
22 | $this->io->write(
23 | "🌳 {$count} packages you are using with a Treeware licence>"
24 | );
25 | $this->io->write('🌳 use the `composer treeware` command to find out more!');
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "treeware/plant",
3 | "type": "composer-plugin",
4 | "license": "MIT",
5 | "description": "",
6 | "require": {
7 | "composer-plugin-api": "^1.0|^2.0"
8 | },
9 | "require-dev": {
10 | "composer/composer": "^1.0|^2.0",
11 | "symfony/console": "^5.1",
12 | "phpstan/phpstan": "^0.12",
13 | "pestphp/pest": "^1.0",
14 | "spatie/pest-plugin-snapshots": "^1.0",
15 | "symplify/easy-coding-standard": "^9.2"
16 | },
17 | "autoload": {
18 | "psr-4": {
19 | "Treeware\\Plant\\": "src/"
20 | }
21 | },
22 | "autoload-dev": {
23 | "psr-4": {
24 | "Tests\\": "tests/"
25 | }
26 | },
27 | "scripts": {
28 | "stan": "phpstan analyse src --level=5",
29 | "ecs": "vendor/bin/ecs",
30 | "fix": "vendor/bin/ecs --fix",
31 | "test": "vendor/bin/pest",
32 | "test-reset": "vendor/bin/pest -d --update-snapshots"
33 | },
34 | "extra": {
35 | "class": "Treeware\\Plant\\Plugin"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/PackageStatsClient.php:
--------------------------------------------------------------------------------
1 | [
16 | 'timeout' => 2,
17 |
18 | ],
19 | ]);
20 |
21 | try {
22 | $response = file_get_contents(
23 | sprintf('%s?ref=%s', self::STATS_ENDPOINT, $ref),
24 | false,
25 | $context
26 | );
27 |
28 | if ($response !== null) {
29 | $jsonObj = json_decode($response, false, 2, JSON_THROW_ON_ERROR);
30 | return $jsonObj->total;
31 | }
32 | } catch (Exception $e) {
33 | }
34 |
35 | return null;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Command/TreewareCommand.php:
--------------------------------------------------------------------------------
1 | getComposer(), new PackageStatsClient());
21 | $packages = $repo->getTreewareWithStats();
22 |
23 | (new PackageList($output, $packages))->show();
24 |
25 | return self::SUCCESS;
26 | }
27 |
28 | protected function configure(): void
29 | {
30 | $this->setName('treeware');
31 | $this->setDescription('List installed Treeware packages.');
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Treeware
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/src/Output/PackageList.php:
--------------------------------------------------------------------------------
1 | output = $output;
22 | $this->packages = $packages;
23 | }
24 |
25 | public function show(): void
26 | {
27 | foreach ($this->packages as $package) {
28 | $this->output->writeln("🌳 {$package->name}");
29 |
30 | foreach ($package->priceGroups as $group => $price) {
31 | $this->output->writeln(" $price ($group)");
32 | }
33 |
34 | $this->output->writeln("⤑ Donate: {$package->url}");
35 |
36 | if ($package->treeCount > 0) {
37 | $this->output->writeln("⤑ Tree count: {$package->treeCount}");
38 | } else {
39 | $this->output->writeln('⤑ No trees donated so far 😢');
40 | }
41 |
42 | $this->output->write(PHP_EOL);
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Output/SinglePackage.php:
--------------------------------------------------------------------------------
1 | io = $io;
23 | $this->package = $package;
24 | }
25 |
26 | public function show(): void
27 | {
28 | $headline = "Treeware licence of {$this->package->name} - {$this->package->description}>";
29 | $underline = str_repeat('-', strlen($headline));
30 | $this->io->write(PHP_EOL);
31 | $this->io->write("🌳 $headline");
32 | $this->io->write("🌳 $underline");
33 |
34 | foreach ($this->package->teaser as $line) {
35 | $this->io->write("🌳 $line");
36 | }
37 |
38 | foreach ($this->package->priceGroups as $group => $price) {
39 | $this->io->write("🌳 ⤑ $price ($group)");
40 | }
41 |
42 | $this->io->write(
43 | "🌳 Donate using this link: {$this->package->url}>" . PHP_EOL
44 | );
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/ecs.php:
--------------------------------------------------------------------------------
1 | services();
16 |
17 | $parameters = $containerConfigurator->parameters();
18 | $parameters->set(Option::PATHS, [__DIR__ . '/src', __DIR__ . '/ecs.php']);
19 |
20 | // exclude paths with really nasty code
21 | $parameters->set(Option::SETS, [
22 | SetList::COMMON,
23 | SetList::CLEAN_CODE,
24 | SetList::PSR_12,
25 | SetList::SYMPLIFY,
26 | SetList::NAMESPACES,
27 | ]);
28 |
29 | $services->set(LineLengthFixer::class)
30 | ->call('configure', [[
31 | LineLengthFixer::LINE_LENGTH => 100,
32 | LineLengthFixer::INLINE_SHORT_LINES => false,
33 | ]]);
34 |
35 | $parameters->set(Option::SKIP, [
36 | ExplicitStringVariableFixer::class => null,
37 | StandardizeHereNowDocKeywordFixer::class => null,
38 | MethodChainingNewlineFixer::class => null,
39 | ]);
40 | };
41 |
--------------------------------------------------------------------------------
/tests/Pest.php:
--------------------------------------------------------------------------------
1 | in('Feature');
15 |
16 | /*
17 | |--------------------------------------------------------------------------
18 | | Expectations
19 | |--------------------------------------------------------------------------
20 | |
21 | | When you're writing tests, you often need to check that values meet certain conditions. The
22 | | "expect()" function gives you access to a set of "expectations" methods that you can use
23 | | to assert different things. Of course, you may extend the Expectation API at any time.
24 | |
25 | */
26 |
27 | expect()->extend('toBeOne', function () {
28 | return $this->toBe(1);
29 | });
30 |
31 | /*
32 | |--------------------------------------------------------------------------
33 | | Functions
34 | |--------------------------------------------------------------------------
35 | |
36 | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your
37 | | project that you don't want to repeat in every file. Here you can also expose helpers as
38 | | global functions to help you to reduce the number of lines of code in your test files.
39 | |
40 | */
41 |
42 | function something()
43 | {
44 | // ..
45 | }
46 |
--------------------------------------------------------------------------------
/tests/OutputTest.php:
--------------------------------------------------------------------------------
1 | io = new \Composer\IO\ConsoleIO($in, $out, $helper);
13 | $this->out = $out;
14 | }
15 | );
16 |
17 |
18 | test('summary for 5 packages', function () {
19 |
20 | (new Summary($this->io))->show(5);
21 | rewind($this->out->getStream());
22 |
23 | $this->assertMatchesSnapshot(stream_get_contents($this->out->getStream()));
24 | });
25 |
26 |
27 | test('single package with default teaser', function () {
28 |
29 | $extra = new \Treeware\Plant\Package(
30 | 'tester/toolbox',
31 | 'Cool stuff in a box'
32 | );
33 |
34 | (new \Treeware\Plant\Output\SinglePackage($this->io, $extra))->show();
35 | rewind($this->out->getStream());
36 |
37 | $this->assertMatchesSnapshot(stream_get_contents($this->out->getStream()));
38 | });
39 |
40 |
41 | test('package list of command', function () {
42 |
43 | $packages = [
44 | new \Treeware\Plant\Package(
45 | 'tester/hot-toolbox',
46 | 'Hot stuff in a box'
47 | ),
48 | (new \Treeware\Plant\Package(
49 | 'tester/cool-toolbox',
50 | 'Cool stuff in a box'
51 | ))->setTreeCount(500)
52 | ];
53 |
54 | (new \Treeware\Plant\Output\PackageList($this->out, $packages))->show();
55 | rewind($this->out->getStream());
56 |
57 | $this->assertMatchesSnapshot(stream_get_contents($this->out->getStream()));
58 | });
59 |
60 |
--------------------------------------------------------------------------------
/src/PackageRepo.php:
--------------------------------------------------------------------------------
1 | repo = $composer->getRepositoryManager()->getLocalRepository();
23 | $this->client = $client;
24 | }
25 |
26 | /**
27 | * @return \Treeware\Plant\Package[]
28 | */
29 | public function getTreeware()
30 | {
31 | $treeware = [];
32 |
33 | /** @var \Composer\Package\CompletePackageInterface[] $installedPackages */
34 | $installedPackages = $this->repo->getPackages();
35 |
36 | foreach ($installedPackages as $package) {
37 | $extra = $package->getExtra();
38 |
39 | if (isset($extra['treeware'])) {
40 | $treeware[$package->getName()] = new Package(
41 | $package->getName(),
42 | $package->getDescription(),
43 | $extra['treeware']['priceGroups'] ?? [],
44 | $extra['treeware']['teaser'] ?? []
45 | );
46 | }
47 | }
48 |
49 | return $treeware;
50 | }
51 |
52 | /**
53 | * @return \Treeware\Plant\Package[]
54 | */
55 | public function getTreewareWithStats(): array
56 | {
57 | $packages = $this->getTreeware();
58 |
59 | foreach ($packages as $package) {
60 | $package->setTreeCount($this->client->getTreeCount($package->name));
61 | }
62 |
63 | return $packages;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # What is this?
2 |
3 | It's a bit like [symfony/thanks](https://github.com/symfony/thanks), but it tries to tackle a bigger problem! **The climate crisis.**
4 |
5 | Open Source can have positive impact on it. With Treeware every donation is a motivation to work on Open Source code.
6 |
7 | The [Treeware idea](https://treeware.earth/about) is great, but it's not very visible. This package tries to solve it.
8 |
9 |
10 | Install
11 | ---
12 |
13 | Add this package as dependency to your package:
14 |
15 | ```sh
16 | $ composer require treeware/plant
17 | ```
18 |
19 | Add an `extra` attribute to your package composer.json that contains at least an empty `treeware` object:
20 |
21 | ```json
22 | {
23 | "extra": {
24 | "treeware": {}
25 | }
26 | }
27 | ```
28 |
29 | Or use this handy shortcut:
30 |
31 | ```sh
32 | composer config extra.treeware --json {}
33 | ```
34 |
35 | To change the default output, add your own `teaser` and `priceGroup` properties:
36 | ```json
37 | {
38 | "extra": {
39 | "treeware": {
40 | "teaser": [
41 | "Your message to the consumers of your package to convince them.",
42 | "Multiple lines are possible, but not more than 3 lines and 200 characters."
43 | ],
44 | "priceGroups": {
45 | "useful": 100,
46 | "important": 250,
47 | "critical": 500
48 | }
49 | }
50 | }
51 | }
52 | ```
53 |
54 |
55 |
56 | ## Example output
57 |
58 | ---
59 |
60 | When others require or update your package using composer, a tiny reminder pops up.
61 |
62 | ```sh
63 |
64 | $ composer require this/fancy-package
65 |
66 | Using version dev-master for this/fancy-package
67 | ./composer.json has been updated
68 | Running composer update this/fancy-package
69 | Loading composer repositories with package information
70 | Updating dependencies
71 | Generating autoload files
72 |
73 |
74 | 🌳 Treeware licence of this/fancy-package - A cool package
75 | 🌳 -------------------------------------------------------------------
76 | 🌳 The author of this open-source software cares about the climate crisis.
77 | 🌳 Using the software in a commercial project requires a donation:
78 | 🌳 ⤑ 100 trees ≈ $17 (useful)
79 | 🌳 ⤑ 250 trees ≈ $42 (important)
80 | 🌳 ⤑ 500 trees ≈ $84 (critical)
81 | 🌳 Donate using this link: https://plant.treeware.earth/this/fancy-package
82 |
83 | ```
84 |
85 |
86 |
--------------------------------------------------------------------------------
/src/Package.php:
--------------------------------------------------------------------------------
1 | 100,
13 | 'important' => 250,
14 | 'critical' => 500,
15 | ];
16 |
17 | public const TEASER_DEFAULT = [
18 | 'The author of this open-source software cares about the climate crisis.',
19 | 'Using the software in a commercial project requires a donation:',
20 | ];
21 |
22 | /**
23 | * @var string
24 | */
25 | public $name;
26 |
27 | /**
28 | * @var string
29 | */
30 | public $description;
31 |
32 | /**
33 | * @var string
34 | */
35 | public $url;
36 |
37 | /**
38 | * @var array
39 | */
40 | public $priceGroups;
41 |
42 | /**
43 | * @var array
44 | */
45 | public $teaser;
46 |
47 | /**
48 | * @var int|null
49 | */
50 | public $treeCount = null;
51 |
52 | public function __construct(
53 | string $name,
54 | string $description,
55 | array $priceGroups = [],
56 | array $teaser = []
57 | ) {
58 | $this->name = $name;
59 | $this->description = $description;
60 | $this->url = sprintf('%s/%s', self::BASE_URL, $name);
61 | $this->assignPriceGroups($priceGroups);
62 | $this->assignTeaser($teaser);
63 | }
64 |
65 | public function setTreeCount(int $trees): self
66 | {
67 | $this->treeCount = $trees;
68 | return $this;
69 | }
70 |
71 | private function assignPriceGroups($priceGroups = []): void
72 | {
73 | if (count($priceGroups) === 0) {
74 | $priceGroups = self::PRICE_DEFAULTS;
75 | }
76 |
77 | foreach ($priceGroups as $group => $trees) {
78 | // Avoid stupid input
79 | if (is_int($trees) && strlen($group) < 15) {
80 | $usd = round($trees * self::USD_PER_TREE);
81 | $this->priceGroups[$group] = sprintf('%d trees ≈ $%d', $trees, $usd);
82 | }
83 | }
84 | }
85 |
86 | private function assignTeaser($teaser = []): void
87 | {
88 | // Avoid stupid input
89 | if (count($teaser) === 0 || count($teaser) > 3) {
90 | $teaser = self::TEASER_DEFAULT;
91 | }
92 | if (strlen(implode(PHP_EOL, $teaser)) > 200) {
93 | $teaser = self::TEASER_DEFAULT;
94 | }
95 |
96 | $this->teaser = $teaser;
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/Plugin.php:
--------------------------------------------------------------------------------
1 | 'showBanner',
41 | ];
42 | }
43 |
44 | /**
45 | * Register treeware command with composer
46 | */
47 | public function getCapabilities(): array
48 | {
49 | return [
50 | CommandProvider::class => Provider::class,
51 | ];
52 | }
53 |
54 | /**
55 | * Initialize Composer plugin
56 | */
57 | public function activate(Composer $composer, IOInterface $io): void
58 | {
59 | $this->composer = $composer;
60 | $this->io = $io;
61 | }
62 |
63 | public function deactivate(Composer $composer, IOInterface $io): void
64 | {
65 | // Not implemented
66 | }
67 |
68 | public function uninstall(Composer $composer, IOInterface $io): void
69 | {
70 | // Not implemented
71 | }
72 |
73 | public function showBanner(): void
74 | {
75 | // Plugin classes do not exist after removing
76 | // but Composer 1 triggers the event: stop here
77 | if (! class_exists('Treeware\Plant\PackageRepo')) {
78 | return;
79 | }
80 |
81 | $filter = $this->getFilteredPackages();
82 | $repo = $this->packageRepo ?? new PackageRepo($this->composer, new PackageStatsClient());
83 | $packages = $repo->getTreeware();
84 | $count = count($packages);
85 |
86 | // No human
87 | if (! $this->io->isInteractive()) {
88 | return;
89 | }
90 |
91 | // No treeware packages
92 | if ($count === 0) {
93 | return;
94 | }
95 |
96 | // Full update: show summary
97 | if (count($filter) === 0) {
98 | (new Summary($this->io))->show($count);
99 | return;
100 | }
101 |
102 | // Filtered package(s): show full info
103 | foreach ($packages as $package) {
104 | if (in_array($package->name, $filter, true)) {
105 | (new SinglePackage($this->io, $package))->show();
106 | }
107 | }
108 | }
109 |
110 | /**
111 | * A list of packages passed to the require or update command If the list is empty, no filter was applied (full
112 | * update)
113 | */
114 | private function getFilteredPackages(): array
115 | {
116 | foreach (debug_backtrace() as $trace) {
117 | if (! isset($trace['object']) || ! isset($trace['args'][0])) {
118 | continue;
119 | }
120 |
121 | if (! $trace['args'][0] instanceof ArgvInput) {
122 | continue;
123 | }
124 |
125 | /** @var ArgvInput $input */
126 | $input = $trace['args'][0];
127 |
128 | return $input->getArgument('packages');
129 | }
130 |
131 | return [];
132 | }
133 | }
134 |
--------------------------------------------------------------------------------