Initial commit

This commit is contained in:
Joop Schilder 2021-04-22 19:26:09 +02:00
commit d673f56c82
19 changed files with 1232 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/vendor/
/.idea/
/var/doc/*
/var/cache/*

31
bin/app.php Normal file
View File

@ -0,0 +1,31 @@
<?php
use Domain\Model\ASRockMemoryQVL;
use Domain\Model\MemoryConfiguration;
use Domain\Normalizer\Decorator\PricewatchDecorator;
use Domain\Normalizer\MemoryConfigurationNormalizer;
use Encoder\EncoderFactory;
use IO\Downloader;
require_once __DIR__ . '/../vendor/autoload.php';
// setup
$list = new ASRockMemoryQVL('AMD', 'X570 Pro4', cpuFamily: 'MS');
$downloader = new Downloader();
$scraper = new MemoryQVLScraper();
// scrape and filter
$page = $downloader->download($list);
$configurations = $scraper->scrape($page);
$selection = $configurations->filter(fn(MemoryConfiguration $memory) => in_array($memory->numberOfModules, [2, 4])
&& $memory->totalSize >= 16
&& $memory->speed >= 3600
&& $memory->overclockingVerified
);
// data presentation
$normalizer = new MemoryConfigurationNormalizer();
$normalizer = PricewatchDecorator::decorate($normalizer);
$encoderFactory = new EncoderFactory($normalizer);
$encoder = $encoderFactory->getEncoder($argv[1] ?? 'csv');
print($encoder->encode($selection));

31
composer.json Normal file
View File

@ -0,0 +1,31 @@
{
"name": "vendor_name/asrock-memory-listing",
"description": "description",
"minimum-stability": "stable",
"license": "proprietary",
"authors": [
{
"name": "joop",
"email": "email@example.com"
}
],
"config": {
"platform": {
"php": "^8.0"
}
},
"autoload": {
"psr-0": {
"": ["src"]
}
},
"require-dev": {
"symfony/var-dumper": "^5.2"
},
"require": {
"symfony/dom-crawler": "^5.2",
"symfony/css-selector": "^5.2",
"ext-curl": "*",
"illuminate/collections": "^8.38"
}
}

737
composer.lock generated Normal file
View File

@ -0,0 +1,737 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "26034160d32c56d1d1d217b4cc155139",
"packages": [
{
"name": "illuminate/collections",
"version": "v8.38.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/collections.git",
"reference": "21690cd5591f2d42d792e5e4a687f9beba829f1d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/collections/zipball/21690cd5591f2d42d792e5e4a687f9beba829f1d",
"reference": "21690cd5591f2d42d792e5e4a687f9beba829f1d",
"shasum": ""
},
"require": {
"illuminate/contracts": "^8.0",
"illuminate/macroable": "^8.0",
"php": "^7.3|^8.0"
},
"suggest": {
"symfony/var-dumper": "Required to use the dump method (^5.1.4)."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "8.x-dev"
}
},
"autoload": {
"psr-4": {
"Illuminate\\Support\\": ""
},
"files": [
"helpers.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "The Illuminate Collections package.",
"homepage": "https://laravel.com",
"support": {
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2021-04-14T11:48:08+00:00"
},
{
"name": "illuminate/contracts",
"version": "v8.38.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/contracts.git",
"reference": "5764f703ea8f74ced163125d810951cd5ef2b7e1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/contracts/zipball/5764f703ea8f74ced163125d810951cd5ef2b7e1",
"reference": "5764f703ea8f74ced163125d810951cd5ef2b7e1",
"shasum": ""
},
"require": {
"php": "^7.3|^8.0",
"psr/container": "^1.0",
"psr/simple-cache": "^1.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "8.x-dev"
}
},
"autoload": {
"psr-4": {
"Illuminate\\Contracts\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "The Illuminate Contracts package.",
"homepage": "https://laravel.com",
"support": {
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2021-04-01T13:09:31+00:00"
},
{
"name": "illuminate/macroable",
"version": "v8.38.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/macroable.git",
"reference": "300aa13c086f25116b5f3cde3ca54ff5c822fb05"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/macroable/zipball/300aa13c086f25116b5f3cde3ca54ff5c822fb05",
"reference": "300aa13c086f25116b5f3cde3ca54ff5c822fb05",
"shasum": ""
},
"require": {
"php": "^7.3|^8.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "8.x-dev"
}
},
"autoload": {
"psr-4": {
"Illuminate\\Support\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "The Illuminate Macroable package.",
"homepage": "https://laravel.com",
"support": {
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2020-10-27T15:20:30+00:00"
},
{
"name": "psr/container",
"version": "1.1.1",
"source": {
"type": "git",
"url": "https://github.com/php-fig/container.git",
"reference": "8622567409010282b7aeebe4bb841fe98b58dcaf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf",
"reference": "8622567409010282b7aeebe4bb841fe98b58dcaf",
"shasum": ""
},
"require": {
"php": ">=7.2.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Psr\\Container\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common Container Interface (PHP FIG PSR-11)",
"homepage": "https://github.com/php-fig/container",
"keywords": [
"PSR-11",
"container",
"container-interface",
"container-interop",
"psr"
],
"support": {
"issues": "https://github.com/php-fig/container/issues",
"source": "https://github.com/php-fig/container/tree/1.1.1"
},
"time": "2021-03-05T17:36:06+00:00"
},
{
"name": "psr/simple-cache",
"version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/php-fig/simple-cache.git",
"reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b",
"reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\SimpleCache\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common interfaces for simple caching",
"keywords": [
"cache",
"caching",
"psr",
"psr-16",
"simple-cache"
],
"support": {
"source": "https://github.com/php-fig/simple-cache/tree/master"
},
"time": "2017-10-23T01:57:42+00:00"
},
{
"name": "symfony/css-selector",
"version": "v5.2.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
"reference": "f65f217b3314504a1ec99c2d6ef69016bb13490f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/css-selector/zipball/f65f217b3314504a1ec99c2d6ef69016bb13490f",
"reference": "f65f217b3314504a1ec99c2d6ef69016bb13490f",
"shasum": ""
},
"require": {
"php": ">=7.2.5"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\CssSelector\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Jean-François Simon",
"email": "jeanfrancois.simon@sensiolabs.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Converts CSS selectors to XPath expressions",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/css-selector/tree/v5.2.4"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-01-27T10:01:46+00:00"
},
{
"name": "symfony/dom-crawler",
"version": "v5.2.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/dom-crawler.git",
"reference": "400e265163f65aceee7e904ef532e15228de674b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/dom-crawler/zipball/400e265163f65aceee7e904ef532e15228de674b",
"reference": "400e265163f65aceee7e904ef532e15228de674b",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-mbstring": "~1.0",
"symfony/polyfill-php80": "^1.15"
},
"conflict": {
"masterminds/html5": "<2.6"
},
"require-dev": {
"masterminds/html5": "^2.6",
"symfony/css-selector": "^4.4|^5.0"
},
"suggest": {
"symfony/css-selector": ""
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\DomCrawler\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Eases DOM navigation for HTML and XML documents",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/dom-crawler/tree/v5.2.4"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-02-15T18:55:04+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.22.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "c6c942b1ac76c82448322025e084cadc56048b4e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/c6c942b1ac76c82448322025e084cadc56048b4e",
"reference": "c6c942b1ac76c82448322025e084cadc56048b4e",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"suggest": {
"ext-ctype": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.22-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"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.22.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-01-07T16:49:33+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.22.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "5232de97ee3b75b0360528dae24e73db49566ab1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/5232de97ee3b75b0360528dae24e73db49566ab1",
"reference": "5232de97ee3b75b0360528dae24e73db49566ab1",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.22-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"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.22.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-01-22T09:19:47+00:00"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.22.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dc3063ba22c2a1fd2f45ed856374d79114998f91",
"reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.22-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"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.22.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-01-07T16:49:33+00:00"
},
{
"name": "symfony/var-dumper",
"version": "v5.2.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
"reference": "89412a68ea2e675b4e44f260a5666729f77f668e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/89412a68ea2e675b4e44f260a5666729f77f668e",
"reference": "89412a68ea2e675b4e44f260a5666729f77f668e",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/polyfill-mbstring": "~1.0",
"symfony/polyfill-php80": "^1.15"
},
"conflict": {
"phpunit/phpunit": "<5.4.3",
"symfony/console": "<4.4"
},
"require-dev": {
"ext-iconv": "*",
"symfony/console": "^4.4|^5.0",
"symfony/process": "^4.4|^5.0",
"twig/twig": "^2.13|^3.0.4"
},
"suggest": {
"ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).",
"ext-intl": "To show region name in time zone dump",
"symfony/console": "To use the ServerDumpCommand and/or the bin/var-dump-server script"
},
"bin": [
"Resources/bin/var-dump-server"
],
"type": "library",
"autoload": {
"files": [
"Resources/functions/dump.php"
],
"psr-4": {
"Symfony\\Component\\VarDumper\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"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": "Provides mechanisms for walking through any arbitrary PHP variable",
"homepage": "https://symfony.com",
"keywords": [
"debug",
"dump"
],
"support": {
"source": "https://github.com/symfony/var-dumper/tree/v5.2.6"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-03-28T09:42:18+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"ext-curl": "*"
},
"platform-dev": [],
"plugin-api-version": "2.0.0"
}

View File

@ -0,0 +1,28 @@
<?php
namespace Domain\Model;
use Stringable;
class ASRockMemoryQVL implements Stringable
{
private string $url;
public function __construct(
string $cpuManufacturer,
string $motherboard,
string $cpuFamily
)
{
$this->url = sprintf('https://www.asrock.com/mb/%s/%s/Memory-%s.asp',
strtoupper(trim($cpuManufacturer)),
rawurlencode(trim($motherboard)),
strtoupper(trim($cpuFamily))
);
}
public function __toString(): string
{
return $this->url;
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace Domain\Model;
class MemoryConfiguration
{
public int $moduleSize;
public int $totalSize;
public string $totalSizeHr;
public int $numberOfModules;
public bool $overclockingVerified;
public bool $dualChannelOverclockingVerified;
public function __construct(
public string $type,
public string $vendor,
public int $speed,
public int $supportedSpeed,
public string $moduleSizeHr,
public string $module,
public string $chip,
public bool $isDualSided,
public string $dimmSocketSupport,
public string $overclockingSupport,
public string $note
)
{
$this->moduleSize = filter_var($this->moduleSizeHr, FILTER_SANITIZE_NUMBER_INT);
$this->overclockingVerified = in_array($this->overclockingSupport, ['v', '2'], true);
$this->dualChannelOverclockingVerified = $this->overclockingSupport === '2';
$this->deriveAdditionalFields();
}
private function deriveAdditionalFields(): void
{
$this->findTotalSize();
$this->numberOfModules = $this->totalSize / $this->moduleSize;
}
private function findTotalSize(): void
{
// Initially 8 times the module size
$this->totalSize = 8 * $this->moduleSize;
// Work our way down to the module size...
while (!str_contains($this->module, $this->totalSize) && $this->totalSize > $this->moduleSize) {
$this->totalSize /= 2;
}
$this->totalSizeHr = sprintf('%dGB', $this->totalSize);
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace Domain\Normalizer\Decorator;
use Domain\Normalizer\Normalizer;
abstract class NormalizerDecorator implements Normalizer
{
private function __construct(
private Normalizer $parent
)
{
}
public static function decorate(Normalizer $normalizer): static
{
return new static($normalizer);
}
abstract protected function getAdditionalHeaders(): array;
abstract public function makePass(array $normalized): array;
final public function getHeaders(): array
{
return array_merge($this->parent->getHeaders(), $this->getAdditionalHeaders());
}
final public function normalize(object $object): array
{
$normalized = $this->parent->normalize($object);
return $this->makePass($normalized);
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace Domain\Normalizer\Decorator;
class PricewatchDecorator extends NormalizerDecorator
{
public function makePass(array $normalized): array
{
$module = $normalized['module'] ?? '-';
$module = $this->selectSignificantTerms($module);
$normalized['pricewatch_url'] = sprintf(
'https://tweakers.net/pricewatch/zoeken/?keyword=%s',
rawurlencode($module)
);
return $normalized;
}
protected function getAdditionalHeaders(): array
{
return ['pricewatch_url'];
}
private function selectSignificantTerms(string $module): string
{
$parts = explode(' ', trim($module), 2);
if ((count($parts) === 2) && stripos($parts[1], 'ver') !== false) {
return $parts[0];
}
return $module;
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace Domain\Normalizer;
use Domain\Model\MemoryConfiguration;
class MemoryConfigurationNormalizer implements Normalizer
{
/**
* @return string[]
*/
public function getHeaders(): array
{
return [
'vendor',
'module',
'type',
'speed',
'supported_speed',
'number_of_modules',
'module_size',
'total_size',
'chip',
'dual_sided',
'dimm_socket_support',
'overclocking_support',
'overclocking_verified',
'dc_overclocking_verified',
'note',
];
}
public function normalize(object $object): array
{
/** @var MemoryConfiguration $object */
return [
'vendor' => $object->vendor,
'module' => $object->module,
'type' => $object->type,
'speed' => $object->speed,
'supported_speed' => $object->supportedSpeed,
'number_of_modules' => $object->numberOfModules,
'module_size' => $object->moduleSizeHr,
'total_size' => $object->totalSizeHr,
'chip' => $object->chip,
'dual_sided' => $object->isDualSided,
'dimm_socket_support' => $object->dimmSocketSupport,
'overclocking_support' => $object->overclockingSupport,
'overclocking_verified' => $object->overclockingVerified,
'dc_overclocking_verified' => $object->dualChannelOverclockingVerified,
'note' => $object->note,
];
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace Domain\Normalizer;
use Domain\Normalizer\Decorator\NormalizerDecorator;
interface Normalizer
{
public function getHeaders(): array;
public function normalize(object $object): array;
}

View File

@ -0,0 +1,48 @@
<?php
namespace Encoder;
use Domain\Normalizer\Normalizer;
use Illuminate\Support\Collection;
class CsvEncoder implements StringEncoder
{
public function __construct(
private Normalizer $normalizer,
private string $columnSeparator = ',',
private string $rowSeparator = PHP_EOL,
)
{
}
public function encode(Collection $items): string
{
$items = $items->values();
$buffer = implode($this->columnSeparator, $this->normalizer->getHeaders());
$buffer .= $this->rowSeparator;
$items->each(function (object $object) use (&$buffer) {
$normalized = $this->normalizer->normalize($object);
$buffer .= collect($normalized)
->map([CsvEncoder::class, 'formatBoolean'])
->map([CsvEncoder::class, 'formatEmpty'])
->implode($this->columnSeparator);
$buffer .= $this->rowSeparator;
});
return $buffer;
}
public static function formatBoolean(mixed $value): mixed
{
if (!is_bool($value)) {
return $value;
}
return true === $value ? 'yes' : 'no';
}
public static function formatEmpty(mixed $value): mixed
{
return empty($value) ? '-' : $value;
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Encoder;
use Domain\Normalizer\Normalizer;
use InvalidArgumentException;
class EncoderFactory
{
public function __construct(
private Normalizer $normalizer
)
{
}
public function getEncoder(string $type): StringEncoder
{
return match ($type) {
'csv', 'CSV' => new CsvEncoder($this->normalizer, columnSeparator: ',', rowSeparator: "\n"),
'json', 'JSON' => new JsonEncoder($this->normalizer),
default => throw new InvalidArgumentException('Invalid encoder: ' . $type)
};
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace Encoder;
use Domain\Normalizer\Normalizer;
use Illuminate\Support\Collection;
class JsonEncoder implements StringEncoder
{
public function __construct(
private Normalizer $normalizer
)
{
}
public function encode(Collection $items): string
{
return json_encode(
[
'count' => $items->count(),
'items' => $items->map([$this->normalizer, 'normalize'])->values()->toArray(),
],
JSON_UNESCAPED_SLASHES
| JSON_UNESCAPED_UNICODE
| JSON_UNESCAPED_LINE_TERMINATORS
| JSON_BIGINT_AS_STRING
| JSON_THROW_ON_ERROR
);
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace Encoder;
use Illuminate\Support\Collection;
interface StringEncoder
{
public function encode(Collection $items): string;
}

55
src/IO/Downloader.php Normal file
View File

@ -0,0 +1,55 @@
<?php
namespace IO;
class Downloader
{
/** @var resource */
private $curl;
public function __construct(
private ?string $cacheDirectory = null
)
{
$this->cacheDirectory ??= dirname(__DIR__, 2) . '/var/cache';
$this->cacheDirectory = rtrim($this->cacheDirectory, DIRECTORY_SEPARATOR);
$this->curl = curl_init();
curl_setopt_array($this->curl, [
CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT x.y; Win64; x64; rv:10.0) Gecko/20100101 Firefox/10.0',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => false,
CURLOPT_FORBID_REUSE => false,
CURLOPT_AUTOREFERER => true,
]);
}
public function __destruct()
{
if (is_resource($this->curl)) {
curl_close($this->curl);
}
}
public function download(string $url, bool $addHtmlTagsToResponse = false): string
{
$file = $this->getCacheFile($url);
if (file_exists($file)) {
return file_get_contents($file);
}
curl_setopt($this->curl, CURLOPT_URL, $url);
$response = curl_exec($this->curl);
if ($addHtmlTagsToResponse) {
$response = sprintf('<!DOCTYPE html><html lang="en"><body>%s</body></html>', $response);
}
file_put_contents($file, $response);
return $response;
}
private function getCacheFile(string $url): string
{
return $this->cacheDirectory . DIRECTORY_SEPARATOR . md5($url) . '.html';
}
}

46
src/MemoryQVLScraper.php Normal file
View File

@ -0,0 +1,46 @@
<?php
use Domain\Model\MemoryConfiguration;
use Illuminate\Support\Collection;
use Symfony\Component\DomCrawler\Crawler;
class MemoryQVLScraper
{
/**
* @param string $html
* @return Collection|MemoryConfiguration[]
*/
public function scrape(string $html): Collection
{
$configurations = [];
$crawler = new Crawler();
$crawler->addHtmlContent($html);
$crawler->filterXPath('//tr')->each(function (Crawler $tr) use (&$configurations) {
$tableData = $tr->filterXPath('//td');
if ($tableData->count() === 0) {
return;
}
$configurations[] = $this->fromTableData($tableData);
});
return collect($configurations);
}
private function fromTableData(Crawler $tableData): MemoryConfiguration
{
return new MemoryConfiguration(
type: $tableData->eq(0)->text('DDR4'),
vendor: $tableData->eq(1)->text(),
speed: (int)$tableData->eq(2)->text(0),
supportedSpeed: (int)$tableData->eq(3)->text(0),
moduleSizeHr: $tableData->eq(4)->text(),
module: $tableData->eq(5)->text(),
chip: $tableData->eq(6)->text(),
isDualSided: $tableData->eq(7)->text() === 'DS',
dimmSocketSupport: $tableData->eq(8)->text(),
overclockingSupport: strtolower($tableData->eq(9)->text('')),
note: $tableData->eq(10)->text()
);
}
}

0
var/.gitignore vendored Normal file
View File

0
var/cache/.gitignore vendored Normal file
View File

0
var/doc/.gitignore vendored Normal file
View File