Initial commit
This commit is contained in:
28
src/Domain/Model/ASRockMemoryQVL.php
Normal file
28
src/Domain/Model/ASRockMemoryQVL.php
Normal 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;
|
||||
}
|
||||
}
|
||||
53
src/Domain/Model/MemoryConfiguration.php
Normal file
53
src/Domain/Model/MemoryConfiguration.php
Normal 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);
|
||||
}
|
||||
}
|
||||
34
src/Domain/Normalizer/Decorator/NormalizerDecorator.php
Normal file
34
src/Domain/Normalizer/Decorator/NormalizerDecorator.php
Normal 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);
|
||||
}
|
||||
}
|
||||
34
src/Domain/Normalizer/Decorator/PricewatchDecorator.php
Normal file
34
src/Domain/Normalizer/Decorator/PricewatchDecorator.php
Normal 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;
|
||||
}
|
||||
}
|
||||
54
src/Domain/Normalizer/MemoryConfigurationNormalizer.php
Normal file
54
src/Domain/Normalizer/MemoryConfigurationNormalizer.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
13
src/Domain/Normalizer/Normalizer.php
Normal file
13
src/Domain/Normalizer/Normalizer.php
Normal 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;
|
||||
|
||||
}
|
||||
48
src/Encoder/CsvEncoder.php
Normal file
48
src/Encoder/CsvEncoder.php
Normal 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;
|
||||
}
|
||||
}
|
||||
24
src/Encoder/EncoderFactory.php
Normal file
24
src/Encoder/EncoderFactory.php
Normal 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)
|
||||
};
|
||||
}
|
||||
}
|
||||
30
src/Encoder/JsonEncoder.php
Normal file
30
src/Encoder/JsonEncoder.php
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
10
src/Encoder/StringEncoder.php
Normal file
10
src/Encoder/StringEncoder.php
Normal 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
55
src/IO/Downloader.php
Normal 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
46
src/MemoryQVLScraper.php
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user