Skip to content

Commit

Permalink
add PHP Framework stats
Browse files Browse the repository at this point in the history
  • Loading branch information
TomasVotruba committed Apr 11, 2019
1 parent 9eedcab commit 1c759ba
Show file tree
Hide file tree
Showing 15 changed files with 2,137 additions and 4 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
@@ -1,6 +1,6 @@
language: php

php: 7.3
php: '7.3'

install:
- composer install
Expand Down
6 changes: 5 additions & 1 deletion ecs.yaml
Expand Up @@ -10,7 +10,7 @@ services:
enable_each_parameter_and_return_inspection: true

Symplify\CodingStandard\Sniffs\CleanCode\CognitiveComplexitySniff:
max_cognitive_complexity: 6
max_cognitive_complexity: 8

parameters:
skip:
Expand All @@ -20,3 +20,7 @@ parameters:
# exists since PHP 7.3, not before
Symplify\CodingStandard\Fixer\Php\ClassStringToClassConstantFixer:
- 'tests/Posts/Year2018/Php73/Php73Test.php'

# mixed[] array redundancy
SlevomatCodingStandard\Sniffs\TypeHints\TypeHintDeclarationSniff.MissingTraversableReturnTypeHintSpecification: ~
SlevomatCodingStandard\Sniffs\TypeHints\TypeHintDeclarationSniff.MissingTraversableParameterTypeHintSpecification: ~
1,581 changes: 1,581 additions & 0 deletions source/_data/generated/php_framework_trends.yaml

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions source/_snippets/footer.twig
Expand Up @@ -19,6 +19,12 @@
</div>

<p>
<strong>
<a href="/php-framework-trends/">PHP Fw Trends</a>
</strong>

<span class="pl-2 pr-2">•</span>

Build by <a href="/thank-you/">{{ contributors|length + 1 }} people</a>

<span class="pl-2 pr-2">•</span>
Expand Down
7 changes: 7 additions & 0 deletions source/assets/css/style.css
Expand Up @@ -585,3 +585,10 @@ table ul {
text-align: center;
font-size: 2em;
}

.thead-inverse th h3 {
color: #DDD;
margin: 0;
font-size: 2em;
line-height: 1.7em;
}
2 changes: 1 addition & 1 deletion source/contact.twig
Expand Up @@ -8,7 +8,7 @@ id: contact
<h1>{{ title }}</h1>

<p class="text-center bigger">
Let me invite you to a great coffee and let's see <strong>how we can help each other</strong> →
Let me invite you to a great coffee to see <strong>how can I help you</strong> →
</p>

<div class="row mt-4 mb-5 ml-sm-0 ml-md-5 " id="contact-points">
Expand Down
78 changes: 78 additions & 0 deletions source/php-framework-trends.twig
@@ -0,0 +1,78 @@
---
layout: "_layouts/default.twig"
title: "PHP Framework Trends"
id: trends
---

<div class="container-fluid" id="blog">
<h1>{{ title }}</h1>

{% for framework in php_framework_trends %}
<table class="table table-bordered">
<thead class="thead-inverse">
<tr>
<th colspan="4" class="text-center">
<h3>{{ framework.name }}</h3>
</th>
</tr>
<tr>
<th>Package</th>
<th class="text-center">
Last month Daily
<p class="text-muted text">
Total {{ framework.vendor_total_last_month|number_format }}
</p>
</th>
<th class="text-center">
Total Last Year
<p class="text-muted text">
Total {{ framework.vendor_total_last_year|number_format }}
</p>
</th>
<th class="text-center">
Trend Last Year
<p class="text-muted text">
{{ framework.average_last_year_trend }} %
</p>
</th>
</tr>
</thead>
{% for package in framework.packages_data %}
<tr>
<th>
{{ package.package_name }}
<small class="text-muted">
<a href="https://packagist.org/packages/{{ package.package_name }}/stats">see downloads</a>
</small>
</th>

<td class="text-right">{{ package.last_month_average_daily_downloads|number_format }}</td>
<td class="text-right">{{ package.last_year_total|number_format }}</td>
<td class="text-right">
<strong>
{% if package.last_year_trend > 0 %}
<span class="text-success">
+{{ package.last_year_trend|number_format }} %
</span>
{% else %}
<span class="text-danger">
{{ package.last_year_trend|number_format }} %
</span>
{% endif %}
</strong>
</td>
</tr>
{% endfor %}
</table>

<br>
<br>
{% endfor %}

<p>Notes</p>

<ul>
<li>packages with <strong>less than 500 downloads</strong> a month were dropped</li>
<li>phpstan/phpstan downloads are cleaned from statistics to prevent miss-leading numbers</li>
</ul>
</div>
34 changes: 34 additions & 0 deletions src/ArrayUtils.php
@@ -0,0 +1,34 @@
<?php declare(strict_types=1);

namespace TomasVotruba\Website;

final class ArrayUtils
{
/**
* @param mixed[] $packagesData
*/
public function getArrayKeyAverage(array $packagesData, string $key): float
{
$total = [];
foreach ($packagesData as $packageData) {
$total[] = $packageData[$key];
}

$average = array_sum($total) / count($total);

return round($average, 2);
}

/**
* @param mixed[] $array
*/
public function getArrayKeySum(array $array, string $key): int
{
$total = 0;
foreach ($array as $item) {
$total += (int) $item[$key];
}

return $total;
}
}
193 changes: 193 additions & 0 deletions src/Command/GeneratePackageStatsCommand.php
@@ -0,0 +1,193 @@
<?php declare(strict_types=1);

namespace TomasVotruba\Website\Command;

use Nette\Utils\DateTime;
use Nette\Utils\Strings;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symplify\PackageBuilder\Console\Command\CommandNaming;
use Symplify\PackageBuilder\Console\ShellCode;
use Symplify\Statie\FileSystem\GeneratedFilesDumper;
use TomasVotruba\Website\ArrayUtils;
use TomasVotruba\Website\Exception\ShouldNotHappenException;
use TomasVotruba\Website\Packagist\PackageMonthlyDownloadsProvider;
use TomasVotruba\Website\Packagist\VendorPackagesProvider;
use TomasVotruba\Website\Statistics;

final class GeneratePackageStatsCommand extends Command
{
/**
* @var string[]
*/
private $frameworkVendorToName = [
'nette' => 'Nette',
'symfony' => 'Symfony',
// laravel
'illuminate' => 'Laravel',
'cakephp' => 'CakePHP',
// single monorepos
'zendframework' => 'Zend',
'yiisoft' => 'Yii',
'codeigniter' => 'Code Igniter',
];

/**
* @var SymfonyStyle
*/
private $symfonyStyle;

/**
* @var GeneratedFilesDumper
*/
private $generatedFilesDumper;

/**
* @var PackageMonthlyDownloadsProvider
*/
private $packageMonthlyDownloadsProvider;

/**
* @var VendorPackagesProvider
*/
private $vendorPackagesProvider;

/**
* @var ArrayUtils
*/
private $arrayUtils;

/**
* @var Statistics
*/
private $statistics;

public function __construct(
SymfonyStyle $symfonyStyle,
GeneratedFilesDumper $generatedFilesDumper,
PackageMonthlyDownloadsProvider $packageMonthlyDownloadsProvider,
VendorPackagesProvider $vendorPackagesProvider,
ArrayUtils $arrayUtils,
Statistics $statistics
) {
parent::__construct();
$this->symfonyStyle = $symfonyStyle;
$this->generatedFilesDumper = $generatedFilesDumper;
$this->packageMonthlyDownloadsProvider = $packageMonthlyDownloadsProvider;
$this->vendorPackagesProvider = $vendorPackagesProvider;
$this->arrayUtils = $arrayUtils;
$this->statistics = $statistics;
}

protected function configure(): void
{
$this->setName(CommandNaming::classToName(self::class));
$this->setDescription('Generates downloads stats data for PHP frameworks');

// @todo include releases, how often, median
// @todo
// - phpstan
// - phpunit
// - php-cs-fixer
// - php code sniffer
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$vendorData = $this->createVendorData();
$this->generatedFilesDumper->dump('php_framework_trends', $vendorData);
$this->symfonyStyle->success('Data imported!');

return ShellCode::SUCCESS;
}

/**
* @return mixed[]
*/
private function createVendorData(): array
{
$vendorData = [];

foreach ($this->frameworkVendorToName as $vendorName => $frameworkName) {
$packagesInVendorJson = $this->vendorPackagesProvider->provideForVendor($vendorName);

$this->symfonyStyle->title(sprintf('Loading data for "%s" vendor', $vendorName));
if (! isset($packagesInVendorJson['packageNames'])) {
throw new ShouldNotHappenException();
}

$packagesData = [];
foreach ($packagesInVendorJson['packageNames'] as $packageName) {
$monthlyDownloadsFromOldestToNewest = $this->packageMonthlyDownloadsProvider->provideForPackage(
$packageName
);

$packageKey = $this->createPackageKey($packageName);

$lastMonthDailyDownloads = $monthlyDownloadsFromOldestToNewest[0];

// few data → skip
if ($lastMonthDailyDownloads <= 500) {
continue;
}

$lastYearTrend = $this->statistics->resolveTrend($monthlyDownloadsFromOldestToNewest, 12);
if ($lastYearTrend === null) {
continue;
}

$packageData = [
'package_name' => $packageName,
'last_month_average_daily_downloads' => $lastMonthDailyDownloads,
'last_year_trend' => $lastYearTrend,
'last_year_total' => $this->statistics->resolveTotal($monthlyDownloadsFromOldestToNewest, 12),
'last_2_years_trend' => $this->statistics->resolveTrend($monthlyDownloadsFromOldestToNewest, 24),
];

$packagesData[$packageKey] = $packageData;
}

$packagesData = $this->sortByLastMonthAverage($packagesData);

$vendorTotalLastMonth = $this->arrayUtils->getArrayKeySum(
$packagesData,
'last_month_average_daily_downloads'
);
$vendorTotalLastYear = $this->arrayUtils->getArrayKeySum($packagesData, 'last_year_total');
$averageLastYearTrend = $this->arrayUtils->getArrayKeyAverage($packagesData, 'last_year_trend');

$vendorData[$vendorName] = [
'name' => $frameworkName,
'updated_at' => (new DateTime())->format('Y-m-d'),
// totals
'vendor_total_last_month' => $vendorTotalLastMonth,
'vendor_total_last_year' => $vendorTotalLastYear,
'average_last_year_trend' => $averageLastYearTrend,
// packages details
'packages_data' => $packagesData,
];
}

return $vendorData;
}

private function createPackageKey(string $packageName): string
{
return Strings::replace($packageName, '#(/|-)#', '_');
}

/**
* @param mixed[] $packagesData
* @return mixed[]
*/
private function sortByLastMonthAverage(array $packagesData): array
{
usort($packagesData, function (array $firstPackage, array $secondPackage) {
return $secondPackage['last_month_average_daily_downloads'] <=> $firstPackage['last_month_average_daily_downloads'];
});

return $packagesData;
}
}
9 changes: 9 additions & 0 deletions src/Exception/ShouldNotHappenException.php
@@ -0,0 +1,9 @@
<?php declare(strict_types=1);

namespace TomasVotruba\Website\Exception;

use Exception;

final class ShouldNotHappenException extends Exception
{
}
14 changes: 14 additions & 0 deletions src/Json/FileToJsonLoader.php
@@ -0,0 +1,14 @@
<?php declare(strict_types=1);

namespace TomasVotruba\Website\Json;

use Nette\Utils\FileSystem;
use Nette\Utils\Json;

final class FileToJsonLoader
{
public function load(string $file): array
{
return Json::decode(FileSystem::read($file), Json::FORCE_ARRAY);
}
}

0 comments on commit 1c759ba

Please sign in to comment.