Add PDO repository (#2)

* Add PDO repository

* Make file storage path configurable

* Name username query property required

* Add special exception class
This commit is contained in:
Anton Komarev
2020-07-12 18:46:42 +03:00
committed by GitHub
parent 042b26ea71
commit e4f24b04f7
13 changed files with 604 additions and 42 deletions

8
.env.example Normal file
View File

@@ -0,0 +1,8 @@
DB_DRIVER=pgsql
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=
DB_NAME=github_profile_views_counter
FILE_STORAGE_PATH=

1
.gitignore vendored
View File

@@ -1 +1,2 @@
/vendor/
.env

View File

@@ -13,7 +13,7 @@ Here is my profile repository: [https://github.com/antonkomarev/antonkomarev](ht
You need to host this app on your server and display it via Markdown syntax:
```markdown
![](https://komarev.com/github-profile-views-counter)
![](https://komarev.com/github-profile-views-counter/?username=antonkomarev)
```
Only `public/index.php` should be exposed.

View File

@@ -19,7 +19,8 @@
}
],
"require": {
"php": "^7.4"
"php": "^7.4",
"vlucas/phpdotenv": "^5.0"
},
"require-dev": {
},

369
composer.lock generated
View File

@@ -4,8 +4,373 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "d6dfebb57666653dd233e3ce73e99b17",
"packages": [],
"content-hash": "26df08e2e3ecf73c0f0c84752718e156",
"packages": [
{
"name": "graham-campbell/result-type",
"version": "v1.0.1",
"source": {
"type": "git",
"url": "https://github.com/GrahamCampbell/Result-Type.git",
"reference": "7e279d2cd5d7fbb156ce46daada972355cea27bb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/7e279d2cd5d7fbb156ce46daada972355cea27bb",
"reference": "7e279d2cd5d7fbb156ce46daada972355cea27bb",
"shasum": ""
},
"require": {
"php": "^7.0|^8.0",
"phpoption/phpoption": "^1.7.3"
},
"require-dev": {
"phpunit/phpunit": "^6.5|^7.5|^8.5|^9.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0-dev"
}
},
"autoload": {
"psr-4": {
"GrahamCampbell\\ResultType\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "graham@alt-three.com"
}
],
"description": "An Implementation Of The Result Type",
"keywords": [
"Graham Campbell",
"GrahamCampbell",
"Result Type",
"Result-Type",
"result"
],
"time": "2020-04-13T13:17:36+00:00"
},
{
"name": "phpoption/phpoption",
"version": "1.7.4",
"source": {
"type": "git",
"url": "https://github.com/schmittjoh/php-option.git",
"reference": "b2ada2ad5d8a32b89088b8adc31ecd2e3a13baf3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/schmittjoh/php-option/zipball/b2ada2ad5d8a32b89088b8adc31ecd2e3a13baf3",
"reference": "b2ada2ad5d8a32b89088b8adc31ecd2e3a13baf3",
"shasum": ""
},
"require": {
"php": "^5.5.9 || ^7.0 || ^8.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.3",
"phpunit/phpunit": "^4.8.35 || ^5.0 || ^6.0 || ^7.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.7-dev"
}
},
"autoload": {
"psr-4": {
"PhpOption\\": "src/PhpOption/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Johannes M. Schmitt",
"email": "schmittjoh@gmail.com"
},
{
"name": "Graham Campbell",
"email": "graham@alt-three.com"
}
],
"description": "Option Type for PHP",
"keywords": [
"language",
"option",
"php",
"type"
],
"time": "2020-06-07T10:40:07+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.17.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "2edd75b8b35d62fd3eeabba73b26b8f1f60ce13d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/2edd75b8b35d62fd3eeabba73b26b8f1f60ce13d",
"reference": "2edd75b8b35d62fd3eeabba73b26b8f1f60ce13d",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"suggest": {
"ext-ctype": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.17-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Ctype\\": ""
},
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for ctype functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"ctype",
"polyfill",
"portable"
],
"time": "2020-06-06T08:46:27+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.17.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "7110338d81ce1cbc3e273136e4574663627037a7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/7110338d81ce1cbc3e273136e4574663627037a7",
"reference": "7110338d81ce1cbc3e273136e4574663627037a7",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.17-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
},
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Mbstring extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"mbstring",
"polyfill",
"portable",
"shim"
],
"time": "2020-06-06T08:46:27+00:00"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.17.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "4a5b6bba3259902e386eb80dd1956181ee90b5b2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/4a5b6bba3259902e386eb80dd1956181ee90b5b2",
"reference": "4a5b6bba3259902e386eb80dd1956181ee90b5b2",
"shasum": ""
},
"require": {
"php": ">=7.0.8"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.17-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Php80\\": ""
},
"files": [
"bootstrap.php"
],
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ion Bazan",
"email": "ion.bazan@gmail.com"
},
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"time": "2020-06-06T08:46:27+00:00"
},
{
"name": "vlucas/phpdotenv",
"version": "v5.0.0",
"source": {
"type": "git",
"url": "https://github.com/vlucas/phpdotenv.git",
"reference": "fbb6a5f65512f21d0db9e21bd49e67f70a9bbd5e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/fbb6a5f65512f21d0db9e21bd49e67f70a9bbd5e",
"reference": "fbb6a5f65512f21d0db9e21bd49e67f70a9bbd5e",
"shasum": ""
},
"require": {
"ext-pcre": "*",
"graham-campbell/result-type": "^1.0.1",
"php": "^7.1.3 || ^8.0",
"phpoption/phpoption": "^1.7.4",
"symfony/polyfill-ctype": "^1.17",
"symfony/polyfill-mbstring": "^1.17",
"symfony/polyfill-php80": "^1.17"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.4.1",
"ext-filter": "*",
"phpunit/phpunit": "^7.5.20 || ^8.5.2 || ^9.0"
},
"suggest": {
"ext-filter": "Required to use the boolean validator."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.0-dev"
}
},
"autoload": {
"psr-4": {
"Dotenv\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Graham Campbell",
"email": "graham@alt-three.com",
"homepage": "https://gjcampbell.co.uk/"
},
{
"name": "Vance Lucas",
"email": "vance@vancelucas.com",
"homepage": "https://vancelucas.com/"
}
],
"description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.",
"keywords": [
"dotenv",
"env",
"environment"
],
"time": "2020-06-07T19:04:14+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "dev",

View File

@@ -0,0 +1,20 @@
<?php
/*
* This file is part of GitHub Profile Views Counter.
*
* (c) Anton Komarev <anton@komarev.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Contracts\Komarev\GitHubProfileViewsCounter;
use Throwable;
interface GitHubProfileViewsCounterThrowableInterface extends Throwable
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of GitHub Profile Views Counter.
*
* (c) Anton Komarev <anton@komarev.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Contracts\Komarev\GitHubProfileViewsCounter;
use InvalidArgumentException;
final class InvalidPathException extends InvalidArgumentException implements
GitHubProfileViewsCounterThrowableInterface
{
}

View File

@@ -0,0 +1,55 @@
<?php
/*
* This file is part of GitHub Profile Views Counter.
*
* (c) Anton Komarev <anton@komarev.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
use Dotenv\Dotenv;
use Komarev\GitHubProfileViewsCounter\CounterImageRendererService;
use Komarev\GitHubProfileViewsCounter\CounterFileRepository;
$basePath = realpath(__DIR__ . '/..');
// Register The Auto Loader
require $basePath . '/vendor/autoload.php';
try {
$dotEnv = Dotenv::createImmutable($basePath);
$dotEnv->safeLoad();
$counterSourceImagePath = $basePath . '/resources/views-count.svg';
if ($_ENV['FILE_STORAGE_PATH'] === null) {
$storagePath = $basePath . '/storage';
} else {
$storagePath = $_ENV['FILE_STORAGE_PATH'];
}
$username = $_GET['username'] ?? '';
$username = trim($username);
if ($username === '') {
throw new InvalidArgumentException('Query property `username` is missing');
}
$counterRepository = new CounterFileRepository($storagePath);
$counterRepository->incrementCountByUsername($username);
$count = $counterRepository->getCountByUsername($username);
$counterImageRenderer = new CounterImageRendererService($counterSourceImagePath);
$counterImage = $counterImageRenderer->getImageWithCount($count);
header('Content-Type: image/svg+xml');
header('Cache-Control: max-age=0, no-cache, no-store, must-revalidate');
echo $counterImage;
} catch (Exception $exception) {
echo $exception->getMessage();
}

View File

@@ -11,29 +11,5 @@
declare(strict_types=1);
use Komarev\GitHubProfileViewsCounter\CounterImageRendererService;
use Komarev\GitHubProfileViewsCounter\CounterFileRepository;
$basePath = realpath(__DIR__ . '/..');
$storagePath = $basePath . '/storage';
$counterSourceImagePath = $basePath . '/resources/views-count.svg';
require $basePath . '/vendor/autoload.php';
$username = $_GET['username'] ?? '';
$username = trim($username);
try {
$counterRepository = new CounterFileRepository($storagePath);
$counterImageRenderer = new CounterImageRendererService($counterSourceImagePath);
$counterRepository->incrementCountByUsername($username);
$count = $counterRepository->getCountByUsername($username);
header('Content-Type: image/svg+xml');
header('Cache-Control: max-age=0, no-cache, no-store, must-revalidate');
echo $counterImageRenderer->getImageWithCount($count);
} catch (Exception $exception) {
echo $exception->getMessage();
}
require 'file-repository.php';
//require 'pdo-repository.php';

70
public/pdo-repository.php Normal file
View File

@@ -0,0 +1,70 @@
<?php
/*
* This file is part of GitHub Profile Views Counter.
*
* (c) Anton Komarev <anton@komarev.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
use Dotenv\Dotenv;
use Dotenv\Exception\InvalidPathException;
use Komarev\GitHubProfileViewsCounter\CounterImageRendererService;
use Komarev\GitHubProfileViewsCounter\CounterPdoRepository;
$basePath = realpath(__DIR__ . '/..');
// Register The Auto Loader
require $basePath . '/vendor/autoload.php';
try {
$dotEnv = Dotenv::createImmutable($basePath);
$dotEnv->load();
$dotEnv->required([
'DB_DRIVER',
'DB_HOST',
'DB_PORT',
'DB_USER',
'DB_PASSWORD',
'DB_NAME',
]);
$counterSourceImagePath = $basePath . '/resources/views-count.svg';
$username = $_GET['username'] ?? '';
$username = trim($username);
if ($username === '') {
throw new InvalidArgumentException('Query property `username` is missing');
}
$dsn = sprintf(
'%s:host=%s;port=%d;dbname=%s',
$_ENV['DB_DRIVER'], $_ENV['DB_HOST'], $_ENV['DB_PORT'], $_ENV['DB_NAME']
);
$dbConnectionOptions = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
];
$dbConnection = new PDO($dsn, $_ENV['DB_USER'], $_ENV['DB_PASSWORD'], $dbConnectionOptions);
$counterRepository = new CounterPdoRepository($dbConnection);
$counterRepository->incrementCountByUsername($username);
$count = $counterRepository->getCountByUsername($username);
$counterImageRenderer = new CounterImageRendererService($counterSourceImagePath);
$counterImage = $counterImageRenderer->getImageWithCount($count);
header('Content-Type: image/svg+xml');
header('Cache-Control: max-age=0, no-cache, no-store, must-revalidate');
echo $counterImage;
} catch (InvalidPathException $exception) {
echo 'Application environment file is missing';
} catch (Exception $exception) {
echo $exception->getMessage();
}

View File

@@ -14,7 +14,7 @@ declare(strict_types=1);
namespace Komarev\GitHubProfileViewsCounter;
use Contracts\Komarev\GitHubProfileViewsCounter\CounterRepositoryInterface;
use InvalidArgumentException;
use Contracts\Komarev\GitHubProfileViewsCounter\InvalidPathException;
final class CounterFileRepository implements CounterRepositoryInterface
{
@@ -23,11 +23,11 @@ final class CounterFileRepository implements CounterRepositoryInterface
public function __construct(string $storagePath)
{
if (!is_dir($storagePath)) {
throw new InvalidArgumentException('Counter storage is not a directory');
throw new InvalidPathException('Counter storage is not a directory');
}
if (!is_writable($storagePath)) {
throw new InvalidArgumentException('Counter storage is not writable');
throw new InvalidPathException('Counter storage is not writable');
}
$this->storagePath = $storagePath;
@@ -49,12 +49,6 @@ final class CounterFileRepository implements CounterRepositoryInterface
private function getCounterFilePath(string $username): string
{
if ($username === '') {
$counterFileName = 'views-count';
} else {
$counterFileName = $username . '-views-count';
}
return $this->storagePath . '/' . $counterFileName;
return $this->storagePath . '/' . $username . '-views-count';
}
}

View File

@@ -13,7 +13,7 @@ declare(strict_types=1);
namespace Komarev\GitHubProfileViewsCounter;
use InvalidArgumentException;
use Contracts\Komarev\GitHubProfileViewsCounter\InvalidPathException;
final class CounterImageRendererService
{
@@ -22,7 +22,7 @@ final class CounterImageRendererService
public function __construct(string $sourceImagePath)
{
if (!file_exists($sourceImagePath)) {
throw new InvalidArgumentException('Counter source image not found');
throw new InvalidPathException('Counter source image not found');
}
$this->sourceImagePath = $sourceImagePath;

View File

@@ -0,0 +1,51 @@
<?php
/*
* This file is part of GitHub Profile Views Counter.
*
* (c) Anton Komarev <anton@komarev.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Komarev\GitHubProfileViewsCounter;
use Contracts\Komarev\GitHubProfileViewsCounter\CounterRepositoryInterface;
use PDO;
final class CounterPdoRepository implements CounterRepositoryInterface
{
private PDO $connection;
public function __construct(PDO $connection)
{
$this->connection = $connection;
}
public function getCountByUsername(string $username): int
{
$statement = $this->connection->prepare(
'SELECT COUNT(*)
FROM page_views
WHERE username = :username;'
);
$statement->bindParam('username', $username);
$statement->execute();
return (int) $statement->fetchColumn(0);
}
public function incrementCountByUsername(string $username): void
{
$statement = $this->connection->prepare(
'INSERT INTO page_views
(username, created_at)
VALUES (:username, NOW());'
);
$statement->bindParam('username', $username);
$statement->execute();
}
}