Initial commit

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

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()
);
}
}