diff --git a/app/console.php b/app/console.php index a80c9b1e..2d5a4056 100644 --- a/app/console.php +++ b/app/console.php @@ -5,7 +5,7 @@ use App\Application\Console\Packagist\GetListCommand; use App\Application\Console\Packagist\GetUpdatesCommand; use App\Application\Console\Packagist\MassImportCommand; -use App\Application\Console\Queue\QueueConsumerCommand; +use App\Application\Console\Queue\QueueConsumeCommand; use DI\ContainerBuilder; use function DI\autowire; @@ -16,7 +16,7 @@ GetListCommand::class => autowire(GetListCommand::class), GetUpdatesCommand::class => autowire(GetUpdatesCommand::class), MassImportCommand::class => autowire(MassImportCommand::class), - QueueConsumerCommand::class => autowire(QueueConsumerCommand::class) + QueueConsumeCommand::class => autowire(QueueConsumeCommand::class) ] ); }; diff --git a/app/repositories.php b/app/repositories.php index 25588251..9a63cb72 100644 --- a/app/repositories.php +++ b/app/repositories.php @@ -4,12 +4,15 @@ use App\Application\Settings\SettingsInterface; use App\Domain\Dependency\DependencyRepositoryInterface; use App\Domain\Package\PackageRepositoryInterface; +use App\Domain\Preference\PreferenceRepositoryInterface; use App\Domain\Stats\StatsRepositoryInterface; use App\Domain\Version\VersionRepositoryInterface; use App\Infrastructure\Persistence\Dependency\CachedDependencyRepository; use App\Infrastructure\Persistence\Dependency\PdoDependencyRepository; use App\Infrastructure\Persistence\Package\CachedPackageRepository; use App\Infrastructure\Persistence\Package\PdoPackageRepository; +use App\Infrastructure\Persistence\Preference\CachedPreferenceRepository; +use App\Infrastructure\Persistence\Preference\PdoPreferenceRepository; use App\Infrastructure\Persistence\Stats\CachedStatsRepository; use App\Infrastructure\Persistence\Stats\PdoStatsRepository; use App\Infrastructure\Persistence\Version\CachedVersionRepository; @@ -50,6 +53,20 @@ $container->get(CacheItemPoolInterface::class) ); }, + // Preference + PdoPreferenceRepository::class => autowire(PdoPreferenceRepository::class), + PreferenceRepositoryInterface::class => static function (ContainerInterface $container): PreferenceRepositoryInterface { + $settings = $container->get(SettingsInterface::class); + + if ($settings->has('cache') === false || $settings->getBool('cache.enabled', false) === false) { + return $container->get(PdoPreferenceRepository::class); + } + + return new CachedPreferenceRepository( + $container->get(PdoPreferenceRepository::class), + $container->get(CacheItemPoolInterface::class) + ); + }, // Stats PdoStatsRepository::class => autowire(PdoStatsRepository::class), StatsRepositoryInterface::class => static function (ContainerInterface $container): StatsRepositoryInterface { diff --git a/bin/console.php b/bin/console.php index 639fcd59..2a1d17ce 100644 --- a/bin/console.php +++ b/bin/console.php @@ -14,7 +14,7 @@ use App\Application\Console\Packagist\GetListCommand; use App\Application\Console\Packagist\GetUpdatesCommand; use App\Application\Console\Packagist\MassImportCommand; -use App\Application\Console\Queue\QueueConsumerCommand; +use App\Application\Console\Queue\QueueConsumeCommand; use DI\ContainerBuilder; use Symfony\Component\Console\Application; use Symfony\Component\Console\CommandLoader\FactoryCommandLoader; @@ -81,8 +81,8 @@ MassImportCommand::getDefaultName() => static function () use ($container): MassImportCommand { return $container->get(MassImportCommand::class); }, - QueueConsumerCommand::getDefaultName() => static function () use ($container): QueueConsumerCommand { - return $container->get(QueueConsumerCommand::class); + QueueConsumeCommand::getDefaultName() => static function () use ($container): QueueConsumeCommand { + return $container->get(QueueConsumeCommand::class); } ] ) diff --git a/db/migrations/20220322224934_preferences.php b/db/migrations/20220322224934_preferences.php new file mode 100644 index 00000000..59816ca3 --- /dev/null +++ b/db/migrations/20220322224934_preferences.php @@ -0,0 +1,19 @@ +table('preferences'); + $preferences + ->addColumn('category', 'text', ['null' => false]) + ->addColumn('property', 'text', ['null' => false]) + ->addColumn('value', 'text', ['null' => false]) + ->addColumn('type', 'text', ['null' => false]) + ->addTimestampsWithTimezone() + ->addIndex(['category', 'property'], ['unique' => true]) + ->addIndex('created_at') + ->create(); + } +} diff --git a/src/Application/Console/Packagist/GetUpdatesCommand.php b/src/Application/Console/Packagist/GetUpdatesCommand.php index 873efbaf..c45566de 100644 --- a/src/Application/Console/Packagist/GetUpdatesCommand.php +++ b/src/Application/Console/Packagist/GetUpdatesCommand.php @@ -3,9 +3,12 @@ namespace App\Application\Console\Packagist; -use App\Domain\Package\Package; +use App\Application\Message\Command\PackageDiscoveryCommand; +use App\Application\Service\Packagist; use App\Domain\Package\PackageRepositoryInterface; -use Buzz\Browser; +use App\Domain\Preference\PreferenceRepositoryInterface; +use App\Domain\Preference\PreferenceTypeEnum; +use Courier\Client\Producer; use Exception; use InvalidArgumentException; use RuntimeException; @@ -22,8 +25,10 @@ final class GetUpdatesCommand extends Command { private const FILE_TIMEOUT = 43200; protected static $defaultName = 'packagist:get-updates'; + private PreferenceRepositoryInterface $preferenceRepository; private PackageRepositoryInterface $packageRepository; - private Browser $browser; + private Packagist $packagist; + private Producer $producer; /** * Command configuration. @@ -67,56 +72,62 @@ protected function execute(InputInterface $input, OutputInterface $output): int throw new InvalidArgumentException('Invalid mirror option'); } - $dataPath = sys_get_temp_dir() . '/changes.json'; + $preferenceCol = $this->preferenceRepository->find( + [ + 'category' => 'packagist', + 'property' => 'timestamp' + ] + ); - $modTime = false; - if (file_exists($dataPath)) { - $modTime = filemtime($dataPath); + $preference = $preferenceCol[0] ?? null; + if ($preference === null) { + $since = $this->packagist->getChangesTimestamp(); + $preference = $this->preferenceRepository->create( + 'packagist', + 'timestamp', + (string)$since, + PreferenceTypeEnum::isInteger + ); } - if ($modTime === false || (time() - $modTime) > self::FILE_TIMEOUT) { - $url = "${mirror}/metadata/changes.json?since=${since}"; - if ($output->isVerbose()) { - $io->text( - sprintf( - "[%s] Downloading a fresh copy of ${url}", - date('H:i:s'), - ) - ); - } + $changes = $this->packagist->getPackageUpdates($preference->getValueAsInteger()); + foreach ($changes['changes'] as $action) { + switch ($action['type']) { + case 'update': + $packageName = $action['package']; + if (str_ends_with($packageName, '~dev')) { + $packageName = substr($packageName, 0, strlen($packageName) - 4); + } - $response = $this->browser->get($url, ['User-Agent' => 'php.package.health (twitter.com/flavioheleno)']); - if ($response->getStatusCode() >= 400) { - throw new RuntimeException( - sprintf( - 'Request to "%s" returned status code %d', - $url, - $response->getStatusCode() - ) - ); - } + $packageCol = $this->packageRepository->find(['name' => $packageName]); - file_put_contents($dataPath, (string)$response->getBody()); - } + $package = $packageCol[0] ?? null; + if ($package === null) { + $package = $this->packageRepository->create($packageName); + } - $json = json_decode(file_get_contents($dataPath), true, 512, JSON_THROW_ON_ERROR); + $this->producer->sendCommand( + new PackageDiscoveryCommand($package) + ); - - foreach ($json['actions'] as $action) { - switch ($action['type']) { - case 'update': break; case 'delete': + // TODO + // $this->producer->sendCommand( + // new PackageRemoveCommand($package); + // ); + break; case 'resync': break; default: // } + } - $package = new Package($packageName, '', '', ''); - - $this->packageRepository->save($package); + $preference = $preference->withIntegerValue($changes['timestamp']); + if ($preference->isDirty()) { + $this->preferenceRepository->update($preference); } $io->text( @@ -146,11 +157,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int } public function __construct( + PreferenceRepositoryInterface $preferenceRepository, PackageRepositoryInterface $packageRepository, - Browser $browser + Packagist $packagist, + Producer $producer ) { - $this->packageRepository = $packageRepository; - $this->browser = $browser; + $this->preferenceRepository = $preferenceRepository; + $this->packageRepository = $packageRepository; + $this->packagist = $packagist; + $this->producer = $producer; parent::__construct(); } diff --git a/src/Application/Console/Queue/QueueConsumerCommand.php b/src/Application/Console/Queue/QueueConsumeCommand.php similarity index 92% rename from src/Application/Console/Queue/QueueConsumerCommand.php rename to src/Application/Console/Queue/QueueConsumeCommand.php index 1f7e39fc..6141f9e1 100644 --- a/src/Application/Console/Queue/QueueConsumerCommand.php +++ b/src/Application/Console/Queue/QueueConsumeCommand.php @@ -14,8 +14,8 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -final class QueueConsumerCommand extends Command { - protected static $defaultName = 'queue:consumer'; +final class QueueConsumeCommand extends Command { + protected static $defaultName = 'queue:consume'; private Consumer $consumer; /** @@ -25,7 +25,7 @@ final class QueueConsumerCommand extends Command { */ protected function configure(): void { $this - ->setDescription('Consume all message bus queues'); + ->setDescription('Consume message bus queues'); } /** diff --git a/src/Application/Processor/Handler/PackageDiscoveryHandler.php b/src/Application/Processor/Handler/PackageDiscoveryHandler.php index ca46a068..312b3050 100644 --- a/src/Application/Processor/Handler/PackageDiscoveryHandler.php +++ b/src/Application/Processor/Handler/PackageDiscoveryHandler.php @@ -6,27 +6,25 @@ use App\Application\Handler\DependencyUpdatedEvent; use App\Application\Message\Event\Dependency\DependencyCreatedEvent; use App\Application\Message\Event\Package\PackageUpdatedEvent; -use App\Application\Message\Event\Stats\StatsCreatedEvent; -use App\Application\Message\Event\Stats\StatsUpdatedEvent; use App\Application\Message\Event\Version\VersionCreatedEvent; use App\Application\Message\Event\Version\VersionUpdatedEvent; use App\Application\Service\Packagist; use App\Domain\Dependency\DependencyRepositoryInterface; use App\Domain\Dependency\DependencyStatusEnum; use App\Domain\Package\PackageRepositoryInterface; -use App\Domain\Stats\StatsRepositoryInterface; use App\Domain\Version\VersionRepositoryInterface; use App\Domain\Version\VersionStatusEnum; use Courier\Client\Producer; use Courier\Message\CommandInterface; use Courier\Processor\Handler\HandlerResultEnum; use Courier\Processor\Handler\InvokeHandlerInterface; +use DateTimeImmutable; +use Exception; use Psr\Log\LoggerInterface; class PackageDiscoveryHandler implements InvokeHandlerInterface { private DependencyRepositoryInterface $dependencyRepository; private PackageRepositoryInterface $packageRepository; - private StatsRepositoryInterface $statsRepository; private VersionRepositoryInterface $versionRepository; private Producer $producer; private Packagist $packagist; @@ -35,7 +33,6 @@ class PackageDiscoveryHandler implements InvokeHandlerInterface { public function __construct( DependencyRepositoryInterface $dependencyRepository, PackageRepositoryInterface $packageRepository, - StatsRepositoryInterface $statsRepository, VersionRepositoryInterface $versionRepository, Producer $producer, Packagist $packagist, @@ -43,7 +40,6 @@ public function __construct( ) { $this->dependencyRepository = $dependencyRepository; $this->packageRepository = $packageRepository; - $this->statsRepository = $statsRepository; $this->versionRepository = $versionRepository; $this->producer = $producer; $this->packagist = $packagist; @@ -60,175 +56,148 @@ public function __invoke(CommandInterface $command): HandlerResultEnum { try { $package = $command->getPackage(); - $metadata = $this->packagist->getPackageMetadataVersion1($package->getName()); + $packageName = $package->getName(); - $package = $package - ->withDescription($metadata['description'] ?? '') - ->withUrl($metadata['repository'] ?? ''); - if ($package->isDirty()) { - $package = $this->packageRepository->update($package); + $pkgs = [ + // dev versions + "${packageName}~dev", + // tagged releses + $packageName + ]; - $this->producer->sendEvent( - new PackageUpdatedEvent($package) - ); - } - - $statsCol = $this->statsRepository->find( - [ - 'package_name' => $package->getName() - ] - ); - - $stats = $statsCol[0] ?? null; - if ($stats === null) { - $stats = $this->statsRepository->create( - $package->getName() - ); - - $this->producer->sendEvent( - new StatsCreatedEvent($stats) - ); - } - - $stats = $stats - ->withGithubStars($metadata['github_stars'] ?? 0) - ->withGithubWatchers($metadata['github_watchers'] ?? 0) - ->withGithubForks($metadata['github_forks'] ?? 0) - ->withDependents($metadata['dependents'] ?? 0) - ->withSuggesters($metadata['suggesters'] ?? 0) - ->withFavers($metadata['favers'] ?? 0) - ->withTotalDownloads($metadata['downloads']['total'] ?? 0) - ->withMonthlyDownloads($metadata['downloads']['monthly'] ?? 0) - ->withDailyDownloads($metadata['downloads']['daily'] ?? 0); - - if ($stats->isDirty()) { - $stats = $this->statsRepository->update($stats); - - $this->producer->sendEvent( - new StatsUpdatedEvent($stats) - ); - } - - // version list is empty - if (count($metadata['versions']) === 0) { - $this->logger->notice('Version list is empty', [$package]); - - return HandlerResultEnum::Accept; - } - - foreach (array_reverse($metadata['versions']) as $release) { - // exclude branches from tagged releases (https://getcomposer.org/doc/articles/versions.md#branches) - $isBranch = preg_match('/^dev-|-dev$/', $release['version']) === 1; - - // find by the unique constraint (number, package_name) - $versionCol = $this->versionRepository->find( - [ - 'number' => $release['version'], - 'package_name' => $package->getName() - ] - ); - - $version = $versionCol[0] ?? null; - if ($version === null) { - $version = $this->versionRepository->create( - $release['version'], - $release['version_normalized'], - $package->getName(), - $isBranch === false - ); - - $this->producer->sendEvent( - new VersionCreatedEvent($version) - ); + foreach ($pkgs as $pkg) { + $metadata = $this->packagist->getPackageMetadataVersion2($pkg); + if (count($metadata) === 0) { + continue; } - // track "require" dependencies - $filteredRequire = array_filter( - $release['require'] ?? [], - static function (string $key): bool { - return preg_match('/^(php|hhvm|ext-.*|lib-.*|pear-.*)$/', $key) !== 1 && - preg_match('/^[^\/]+\/[^\/]+$/', $key) === 1; - }, - ARRAY_FILTER_USE_KEY - ); - - // flag packages without require dependencies with VersionStatusEnum::NoDeps - if (empty($filteredRequire)) { - $version = $version->withStatus(VersionStatusEnum::NoDeps); - $version = $this->versionRepository->update($version); + $package = $package + ->withDescription($metadata[0]['description'] ?? '') + ->withUrl($metadata[0]['source']['url'] ?? ''); + if ($package->isDirty()) { + $package = $this->packageRepository->update($package); $this->producer->sendEvent( - new VersionUpdatedEvent($version) + new PackageUpdatedEvent($package) ); } - foreach ($filteredRequire as $dependencyName => $constraint) { - if ($constraint === 'self.version') { - // need to find out how to handle this - continue; - } + foreach (array_reverse($metadata) as $release) { + // exclude branches from tagged releases (https://getcomposer.org/doc/articles/versions.md#branches) + $isBranch = preg_match('/^dev-|-dev$/', $release['version']) === 1; - // find by the unique constraint (version_id, name, development) - $dependencyCol = $this->dependencyRepository->find( + // find by the unique constraint (number, package_name) + $versionCol = $this->versionRepository->find( [ - 'version_id' => $version->getId(), - 'name' => $dependencyName, - 'development' => false + 'number' => $release['version'], + 'package_name' => $packageName ] ); - $dependency = $dependencyCol[0] ?? null; - if ($dependency === null) { - $dependency = $this->dependencyRepository->create( - $version->getId(), - $dependencyName, - $constraint, - false + $version = $versionCol[0] ?? null; + if ($version === null) { + $version = $this->versionRepository->create( + $release['version'], + $release['version_normalized'], + $packageName, + $isBranch === false, + VersionStatusEnum::Unknown, + new DateTimeImmutable($release['time'] ?? 'now') ); $this->producer->sendEvent( - new DependencyCreatedEvent($dependency) + new VersionCreatedEvent($version) ); } - } - // track "require-dev" dependencies - $filteredRequireDev = array_filter( - $release['require-dev'] ?? [], - static function (string $key): bool { - return preg_match('/^(php|hhvm|ext-.*|lib-.*|pear-.*)$/', $key) !== 1 && - preg_match('/^[^\/]+\/[^\/]+$/', $key) === 1; - }, - ARRAY_FILTER_USE_KEY - ); - - foreach ($filteredRequireDev as $dependencyName => $constraint) { - if ($constraint === 'self.version') { - // need to find out how to handle this - continue; + // track "require" dependencies + $filteredRequire = array_filter( + $release['require'] ?? [], + static function (string $key): bool { + return preg_match('/^(php|hhvm|ext-.*|lib-.*|pear-.*)$/', $key) !== 1 && + preg_match('/^[^\/]+\/[^\/]+$/', $key) === 1; + }, + ARRAY_FILTER_USE_KEY + ); + + // flag packages without require dependencies with VersionStatusEnum::NoDeps + if (empty($filteredRequire)) { + $version = $version->withStatus(VersionStatusEnum::NoDeps); + $version = $this->versionRepository->update($version); + + $this->producer->sendEvent( + new VersionUpdatedEvent($version) + ); } - // find by the unique constraint (version_id, name, development) - $dependencyCol = $this->dependencyRepository->find( - [ - 'version_id' => $version->getId(), - 'name' => $dependencyName, - 'development' => true - ] + foreach ($filteredRequire as $dependencyName => $constraint) { + if ($constraint === 'self.version') { + // need to find out how to handle this + continue; + } + + // find by the unique constraint (version_id, name, development) + $dependencyCol = $this->dependencyRepository->find( + [ + 'version_id' => $version->getId(), + 'name' => $dependencyName, + 'development' => false + ] + ); + + $dependency = $dependencyCol[0] ?? null; + if ($dependency === null) { + $dependency = $this->dependencyRepository->create( + $version->getId(), + $dependencyName, + $constraint, + false + ); + + $this->producer->sendEvent( + new DependencyCreatedEvent($dependency) + ); + } + } + + // track "require-dev" dependencies + $filteredRequireDev = array_filter( + $release['require-dev'] ?? [], + static function (string $key): bool { + return preg_match('/^(php|hhvm|ext-.*|lib-.*|pear-.*)$/', $key) !== 1 && + preg_match('/^[^\/]+\/[^\/]+$/', $key) === 1; + }, + ARRAY_FILTER_USE_KEY ); - $dependency = $dependencyCol[0] ?? null; - if ($dependency === null) { - $dependency = $this->dependencyRepository->create( - $version->getId(), - $dependencyName, - $constraint, - true + foreach ($filteredRequireDev as $dependencyName => $constraint) { + if ($constraint === 'self.version') { + // need to find out how to handle this + continue; + } + + // find by the unique constraint (version_id, name, development) + $dependencyCol = $this->dependencyRepository->find( + [ + 'version_id' => $version->getId(), + 'name' => $dependencyName, + 'development' => true + ] ); - $this->producer->sendEvent( - new DependencyCreatedEvent($dependency) - ); + $dependency = $dependencyCol[0] ?? null; + if ($dependency === null) { + $dependency = $this->dependencyRepository->create( + $version->getId(), + $dependencyName, + $constraint, + true + ); + + $this->producer->sendEvent( + new DependencyCreatedEvent($dependency) + ); + } } } } diff --git a/src/Application/Service/Packagist.php b/src/Application/Service/Packagist.php index 919223a1..bfc43eab 100644 --- a/src/Application/Service/Packagist.php +++ b/src/Application/Service/Packagist.php @@ -113,6 +113,10 @@ public function getPackageMetadataVersion2( ); $json = json_decode($content, true, 512, JSON_THROW_ON_ERROR); + if (str_ends_with($packageName, '~dev')) { + $packageName = substr($packageName, 0, strlen($packageName) - 4); + } + if (isset($json['packages'][$packageName]) === false) { throw new RuntimeException('Invalid package metadata v2 format'); } @@ -122,16 +126,16 @@ public function getPackageMetadataVersion2( public function getPackageUpdates(int $since = 0, string $mirror = 'https://packagist.org'): array { $content = $this->updateFileContent( - "packagist/updates.json", + 'packagist/updates.json', "${mirror}/metadata/changes.json?since=${since}" ); $json = json_decode($content, true, 512, JSON_THROW_ON_ERROR); - if (isset($json['actions']) === false) { + if (isset($json['actions']) === false || isset($json['timestamp']) === false) { throw new RuntimeException('Invalid package updates format'); } - return $json['actions']; + return $json; } public function getSecurityAdvisories(string $packageName, string $mirror = 'https://packagist.org'): array { diff --git a/src/Domain/Dependency/Dependency.php b/src/Domain/Dependency/Dependency.php index e9c00fb6..13fe583c 100644 --- a/src/Domain/Dependency/Dependency.php +++ b/src/Domain/Dependency/Dependency.php @@ -25,7 +25,7 @@ public function __construct( string $constraint, bool $development = false, DependencyStatusEnum $status = DependencyStatusEnum::Unknown, - DateTimeImmutable $createdAt = null, + DateTimeImmutable $createdAt = new DateTimeImmutable(), DateTimeImmutable $updatedAt = null ) { $this->id = $id; @@ -34,7 +34,7 @@ public function __construct( $this->constraint = $constraint; $this->development = $development; $this->status = $status; - $this->createdAt = $createdAt ?? new DateTimeImmutable(); + $this->createdAt = $createdAt; $this->updatedAt = $updatedAt; } @@ -148,7 +148,7 @@ public function jsonSerialize(): array { 'development' => $this->development, 'status' => $this->status->value, 'created_at' => $this->createdAt->getTimestamp(), - 'updated_at' => $this->updatedAt?->getTimestamp(), + 'updated_at' => $this->updatedAt?->getTimestamp() ]; } } diff --git a/src/Domain/Dependency/DependencyRepositoryInterface.php b/src/Domain/Dependency/DependencyRepositoryInterface.php index 453a89bd..3d63a529 100644 --- a/src/Domain/Dependency/DependencyRepositoryInterface.php +++ b/src/Domain/Dependency/DependencyRepositoryInterface.php @@ -3,14 +3,16 @@ namespace App\Domain\Dependency; -interface DependencyRepositoryInterface { +use DateTimeImmutable; +interface DependencyRepositoryInterface { public function create( int $versionId, string $name, string $constraint, bool $development = false, - DependencyStatusEnum $status = DependencyStatusEnum::Unknown + DependencyStatusEnum $status = DependencyStatusEnum::Unknown, + DateTimeImmutable $createdAt = new DateTimeImmutable() ): Dependency; public function all(): DependencyCollection; diff --git a/src/Domain/Package/Package.php b/src/Domain/Package/Package.php index 9a94aeba..b96cd123 100644 --- a/src/Domain/Package/Package.php +++ b/src/Domain/Package/Package.php @@ -23,14 +23,14 @@ public function __construct( string $description, string $latestVersion, string $url, - DateTimeImmutable $createdAt = null, + DateTimeImmutable $createdAt = new DateTimeImmutable(), DateTimeImmutable $updatedAt = null ) { $this->name = $name; $this->description = $description; $this->latestVersion = $latestVersion; $this->url = $url; - $this->createdAt = $createdAt ?? new DateTimeImmutable(); + $this->createdAt = $createdAt; $this->updatedAt = $updatedAt; [$this->vendor, $this->project] = explode('/', $name); @@ -129,7 +129,7 @@ public function jsonSerialize(): array { 'latest_version' => $this->latestVersion, 'url' => $this->url, 'created_at' => $this->createdAt->getTimestamp(), - 'updated_at' => $this->updatedAt?->getTimestamp(), + 'updated_at' => $this->updatedAt?->getTimestamp() ]; } } diff --git a/src/Domain/Package/PackageRepositoryInterface.php b/src/Domain/Package/PackageRepositoryInterface.php index 22da2a13..fa3e48e7 100644 --- a/src/Domain/Package/PackageRepositoryInterface.php +++ b/src/Domain/Package/PackageRepositoryInterface.php @@ -3,8 +3,13 @@ namespace App\Domain\Package; +use DateTimeImmutable; + interface PackageRepositoryInterface { - public function create(string $name): Package; + public function create( + string $name, + DateTimeImmutable $createdAt = new DateTimeImmutable() + ): Package; public function all(): PackageCollection; diff --git a/src/Domain/Preference/Preference.php b/src/Domain/Preference/Preference.php new file mode 100644 index 00000000..fac1a6f1 --- /dev/null +++ b/src/Domain/Preference/Preference.php @@ -0,0 +1,145 @@ +id = $id; + $this->category = $category; + $this->property = $property; + $this->value = $value; + $this->type = $type; + $this->createdAt = $createdAt; + $this->updatedAt = $updatedAt; + } + + public function getId(): ?int { + return $this->id; + } + + public function getCategory(): string { + return $this->category; + } + + public function getProperty(): string { + return $this->property; + } + + public function getValueAsString(): string { + return $this->value; + } + + public function withStringValue(string $value): self { + $clone = clone $this; + $clone->value = $value; + $clone->type = PreferenceTypeEnum::isString; + $clone->dirty = true; + $clone->updatedAt = new DateTimeImmutable(); + + return $clone; + } + + public function getValueAsInteger(): int { + return (int)$this->value; + } + + public function withIntegerValue(int $value): self { + $clone = clone $this; + $clone->value = (string)$value; + $clone->type = PreferenceTypeEnum::isInteger; + $clone->dirty = true; + $clone->updatedAt = new DateTimeImmutable(); + + return $clone; + } + + public function getValueAsFloat(): float { + return (float)$this->value; + } + + public function withFloatValue(float $value): self { + $clone = clone $this; + $clone->value = (string)$value; + $clone->type = PreferenceTypeEnum::isFloat; + $clone->dirty = true; + $clone->updatedAt = new DateTimeImmutable(); + + return $clone; + } + + public function getValueAsBool(): bool { + return (bool)$this->value; + } + + public function withBoolValue(bool $value): self { + $clone = clone $this; + $clone->value = (string)$value; + $clone->type = PreferenceTypeEnum::isBool; + $clone->dirty = true; + $clone->updatedAt = new DateTimeImmutable(); + + return $clone; + } + + public function getType(): PreferenceTypeEnum { + return $this->type; + } + + public function getCreatedAt(): DateTimeImmutable { + return $this->createdAt; + } + + public function getUpdatedAt(): ?DateTimeImmutable { + return $this->updatedAt; + } + + public function isDirty(): bool { + return $this->dirty; + } + + /** + * @return array{ + * id: int|null, + * category: string, + * property: string, + * value: string, + * type: string, + * created_at: int, + * updated_at: int|null + * } + */ + #[ReturnTypeWillChange] + public function jsonSerialize(): array { + return [ + 'id' => $this->id, + 'category' => $this->category, + 'property' => $this->property, + 'value' => $this->value, + 'type' => $this->type->value, + 'created_at' => $this->createdAt->getTimestamp(), + 'updated_at' => $this->updatedAt?->getTimestamp() + ]; + } +} diff --git a/src/Domain/Preference/PreferenceCollection.php b/src/Domain/Preference/PreferenceCollection.php new file mode 100644 index 00000000..f223e7e2 --- /dev/null +++ b/src/Domain/Preference/PreferenceCollection.php @@ -0,0 +1,15 @@ + + */ +final class PreferenceCollection extends AbstractCollection { + public function getType(): string { + return Preference::class; + } +} diff --git a/src/Domain/Preference/PreferenceNotFoundException.php b/src/Domain/Preference/PreferenceNotFoundException.php new file mode 100644 index 00000000..7d3bf416 --- /dev/null +++ b/src/Domain/Preference/PreferenceNotFoundException.php @@ -0,0 +1,13 @@ +packageName = $packageName; @@ -46,7 +46,7 @@ public function __construct( $this->totalDownloads = $totalDownloads; $this->monthlyDownloads = $monthlyDownloads; $this->dailyDownloads = $dailyDownloads; - $this->createdAt = $createdAt ?? new DateTimeImmutable(); + $this->createdAt = $createdAt; $this->updatedAt = $updatedAt; } @@ -249,7 +249,7 @@ public function jsonSerialize(): array { 'monthly_downloads' => $this->monthlyDownloads, 'daily_downloads' => $this->dailyDownloads, 'created_at' => $this->createdAt->getTimestamp(), - 'updated_at' => $this->updatedAt?->getTimestamp(), + 'updated_at' => $this->updatedAt?->getTimestamp() ]; } } diff --git a/src/Domain/Stats/StatsRepositoryInterface.php b/src/Domain/Stats/StatsRepositoryInterface.php index a98ea7e5..c8e258ba 100644 --- a/src/Domain/Stats/StatsRepositoryInterface.php +++ b/src/Domain/Stats/StatsRepositoryInterface.php @@ -3,6 +3,8 @@ namespace App\Domain\Stats; +use DateTimeImmutable; + interface StatsRepositoryInterface { public function create( string $packageName, @@ -14,7 +16,8 @@ public function create( int $favers = 0, int $totalDownloads = 0, int $monthlyDownloads = 0, - int $dailyDownloads = 0 + int $dailyDownloads = 0, + DateTimeImmutable $createdAt = new DateTimeImmutable() ): Stats; public function all(): StatsCollection; diff --git a/src/Domain/Version/Version.php b/src/Domain/Version/Version.php index bbb44772..ddb07ee6 100644 --- a/src/Domain/Version/Version.php +++ b/src/Domain/Version/Version.php @@ -25,7 +25,7 @@ public function __construct( string $packageName, bool $release = false, VersionStatusEnum $status = VersionStatusEnum::Unknown, - DateTimeImmutable $createdAt = null, + DateTimeImmutable $createdAt = new DateTimeImmutable(), DateTimeImmutable $updatedAt = null ) { $this->id = $id; @@ -34,7 +34,7 @@ public function __construct( $this->packageName = $packageName; $this->release = $release; $this->status = $status; - $this->createdAt = $createdAt ?? new DateTimeImmutable(); + $this->createdAt = $createdAt; $this->updatedAt = $updatedAt; } @@ -159,7 +159,7 @@ public function jsonSerialize(): array { 'release' => $this->release, 'status' => $this->status->value, 'created_at' => $this->createdAt->getTimestamp(), - 'updated_at' => $this->updatedAt?->getTimestamp(), + 'updated_at' => $this->updatedAt?->getTimestamp() ]; } } diff --git a/src/Domain/Version/VersionRepositoryInterface.php b/src/Domain/Version/VersionRepositoryInterface.php index 18ac276f..018aa05a 100644 --- a/src/Domain/Version/VersionRepositoryInterface.php +++ b/src/Domain/Version/VersionRepositoryInterface.php @@ -3,14 +3,16 @@ namespace App\Domain\Version; -interface VersionRepositoryInterface { +use DateTimeImmutable; +interface VersionRepositoryInterface { public function create( string $number, string $normalized, string $packageName, bool $release, - VersionStatusEnum $status = VersionStatusEnum::Unknown + VersionStatusEnum $status = VersionStatusEnum::Unknown, + DateTimeImmutable $createdAt = new DateTimeImmutable() ): Version; public function all(): VersionCollection; diff --git a/src/Infrastructure/Persistence/Dependency/CachedDependencyRepository.php b/src/Infrastructure/Persistence/Dependency/CachedDependencyRepository.php index 533722cb..c415a3c2 100644 --- a/src/Infrastructure/Persistence/Dependency/CachedDependencyRepository.php +++ b/src/Infrastructure/Persistence/Dependency/CachedDependencyRepository.php @@ -7,6 +7,7 @@ use App\Domain\Dependency\DependencyCollection; use App\Domain\Dependency\DependencyRepositoryInterface; use App\Domain\Dependency\DependencyStatusEnum; +use DateTimeImmutable; use Psr\Cache\CacheItemPoolInterface; final class CachedDependencyRepository implements DependencyRepositoryInterface { @@ -26,14 +27,16 @@ public function create( string $name, string $constraint, bool $development = false, - DependencyStatusEnum $status = DependencyStatusEnum::Unknown + DependencyStatusEnum $status = DependencyStatusEnum::Unknown, + DateTimeImmutable $createdAt = new DateTimeImmutable() ): Dependency { return $this->dependencyRepository->create( $versionId, $name, $constraint, $development, - $status + $status, + $createdAt ); } diff --git a/src/Infrastructure/Persistence/Dependency/PdoDependencyRepository.php b/src/Infrastructure/Persistence/Dependency/PdoDependencyRepository.php index 13c06a84..03da4e91 100644 --- a/src/Infrastructure/Persistence/Dependency/PdoDependencyRepository.php +++ b/src/Infrastructure/Persistence/Dependency/PdoDependencyRepository.php @@ -49,7 +49,8 @@ public function create( string $name, string $constraint, bool $development = false, - DependencyStatusEnum $status = DependencyStatusEnum::Unknown + DependencyStatusEnum $status = DependencyStatusEnum::Unknown, + DateTimeImmutable $createdAt = new DateTimeImmutable() ): Dependency { return $this->save( new Dependency( @@ -59,13 +60,26 @@ public function create( $constraint, $development, $status, - new DateTimeImmutable() + $createdAt ) ); } public function all(): DependencyCollection { - return new DependencyCollection(); + $stmt = $this->pdo->query( + <<fetch(PDO::FETCH_ASSOC)) { + $dependencyCol->add($this->hydrate($row)); + } + + return $dependencyCol; } public function get(int $id): Dependency { diff --git a/src/Infrastructure/Persistence/Package/CachedPackageRepository.php b/src/Infrastructure/Persistence/Package/CachedPackageRepository.php index 0b3ed3bd..d10b1fac 100644 --- a/src/Infrastructure/Persistence/Package/CachedPackageRepository.php +++ b/src/Infrastructure/Persistence/Package/CachedPackageRepository.php @@ -6,6 +6,7 @@ use App\Domain\Package\Package; use App\Domain\Package\PackageCollection; use App\Domain\Package\PackageRepositoryInterface; +use DateTimeImmutable; use Psr\Cache\CacheItemPoolInterface; final class CachedPackageRepository implements PackageRepositoryInterface { @@ -20,8 +21,14 @@ public function __construct( $this->cacheItemPool = $cacheItemPool; } - public function create(string $name): Package { - return $this->packageRepository->create($name); + public function create( + string $name, + DateTimeImmutable $createdAt = new DateTimeImmutable() + ): Package { + return $this->packageRepository->create( + $name, + $createdAt + ); } public function all(): PackageCollection { diff --git a/src/Infrastructure/Persistence/Package/PdoPackageRepository.php b/src/Infrastructure/Persistence/Package/PdoPackageRepository.php index 0f28c61e..36b388bf 100644 --- a/src/Infrastructure/Persistence/Package/PdoPackageRepository.php +++ b/src/Infrastructure/Persistence/Package/PdoPackageRepository.php @@ -39,29 +39,29 @@ public function __construct(PDO $pdo) { $this->pdo = $pdo; } - public function create(string $name): Package { + public function create( + string $name, + DateTimeImmutable $createdAt = new DateTimeImmutable() + ): Package { return $this->save( new Package( $name, '', '', '', - new DateTimeImmutable() + $createdAt ) ); } public function all(): PackageCollection { - static $stmt = null; - if ($stmt === null) { - $stmt = $this->pdo->query( - <<pdo->query( + <<fetch(PDO::FETCH_ASSOC)) { diff --git a/src/Infrastructure/Persistence/Preference/CachedPreferenceRepository.php b/src/Infrastructure/Persistence/Preference/CachedPreferenceRepository.php new file mode 100644 index 00000000..b3a230c7 --- /dev/null +++ b/src/Infrastructure/Persistence/Preference/CachedPreferenceRepository.php @@ -0,0 +1,105 @@ +preferenceRepository = $preferenceRepository; + $this->cacheItemPool = $cacheItemPool; + } + + public function create( + string $category, + string $property, + string $value, + PreferenceTypeEnum $status = PreferenceTypeEnum::isString, + DateTimeImmutable $createdAt = new DateTimeImmutable() + ): Preference { + return $this->preferenceRepository->create( + $category, + $property, + $value, + $status, + $createdAt + ); + } + + public function all(): PreferenceCollection { + $item = $this->cacheItemPool->getItem('/preference'); + $preferenceCol = $item->get(); + if ($item->isHit() === false) { + $preferenceCol = $this->preferenceRepository->all(); + + $item->set($preferenceCol); + $item->expiresAfter(3600); + + $this->cacheItemPool->save($item); + } + + return $preferenceCol; + } + + /** + * @throws \App\Domain\Preference\PreferenceNotFoundException + */ + public function get(int $id): Preference { + $item = $this->cacheItemPool->getItem("/preference/${id}"); + $preference = $item->get(); + if ($item->isHit() === false ) { + $preference = $this->preferenceRepository->get($id); + + $item->set($preference); + $item->expiresAfter(3600); + + $this->cacheItemPool->save($item); + } + + return $preference; + } + + public function find(array $query): PreferenceCollection { + $key = http_build_query($query); + $item = $this->cacheItemPool->getItem("/preference/find/{$key}"); + $preferenceCol = $item->get(); + if ($item->isHit() === false) { + $preferenceCol = $this->preferenceRepository->find($query); + + $item->set($preferenceCol); + $item->expiresAfter(3600); + + $this->cacheItemPool->save($item); + } + + return $preferenceCol; + } + + public function save(Preference $preference): Preference { + $preference = $this->preferenceRepository->save($preference); + + $this->cacheItemPool->deleteItem('/preference/' . $preference->getId()); + + return $preference; + } + + public function update(Preference $preference): Preference { + $preference = $this->preferenceRepository->update($preference); + + $this->cacheItemPool->deleteItem('/preference/' . $preference->getId()); + + return $preference; + } +} diff --git a/src/Infrastructure/Persistence/Preference/PdoPreferenceRepository.php b/src/Infrastructure/Persistence/Preference/PdoPreferenceRepository.php new file mode 100644 index 00000000..f49be4cc --- /dev/null +++ b/src/Infrastructure/Persistence/Preference/PdoPreferenceRepository.php @@ -0,0 +1,200 @@ +pdo = $pdo; + } + + public function create( + string $category, + string $property, + string $value, + PreferenceTypeEnum $type = PreferenceTypeEnum::isString, + DateTimeImmutable $createdAt = new DateTimeImmutable() + ): Preference { + return $this->save( + new Preference( + null, + $category, + $property, + $value, + $type, + $createdAt + ) + ); + } + + public function all(): PreferenceCollection { + $stmt = $this->pdo->query( + <<fetch(PDO::FETCH_ASSOC)) { + $preferenceCol->add($this->hydrate($row)); + } + + return $preferenceCol; + } + + public function get(int $id): Preference { + static $stmt = null; + if ($stmt === null) { + $stmt = $this->pdo->prepare( + <<execute(['id' => $id]); + if ($stmt->rowCount() === 0) { + throw new PreferenceNotFoundException("Preference '${id}' not found"); + } + + $row = $stmt->fetch(PDO::FETCH_ASSOC); + + return $this->hydrate($row); + } + + public function find(array $query): PreferenceCollection { + $where = []; + foreach (array_keys($query) as $col) { + $where[] = sprintf( + '"%1$s" = :%1$s', + $col + ); + } + + $where = implode(' AND ', $where); + + $stmt = $this->pdo->prepare( + <<execute($query); + + $preferenceCol = new PreferenceCollection(); + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + $preferenceCol->add($this->hydrate($row)); + } + + return $preferenceCol; + } + + public function save(Preference $preference): Preference { + static $stmt = null; + if ($stmt === null) { + $stmt = $this->pdo->prepare( + <<execute( + [ + 'category' => $preference->getCategory(), + 'property' => $preference->getProperty(), + 'value' => $preference->getValueAsString(), + 'type' => $preference->getType()->value, + 'created_at' => $preference->getCreatedAt()->format(DateTimeInterface::ATOM) + ] + ); + + return new Preference( + (int)$this->pdo->lastInsertId('preferences_id_seq'), + $preference->getCategory(), + $preference->getProperty(), + $preference->getValueAsString(), + $preference->getType()->value, + $preference->getCreatedAt() + ); + } + + public function update(Preference $preference): Preference { + static $stmt = null; + if ($stmt === null) { + $stmt = $this->pdo->prepare( + <<isDirty()) { + $stmt->execute( + [ + 'id' => $preference->getId(), + 'category' => $preference->getCategory(), + 'property' => $preference->getProperty(), + 'value' => $preference->getValueAsString(), + 'type' => $preference->getType()->value, + 'updated_at' => $preference->getUpdatedAt()?->format(DateTimeInterface::ATOM) + ] + ); + + return $this->get($preference->getId()); + } + + return $preference; + } +} diff --git a/src/Infrastructure/Persistence/Stats/CachedStatsRepository.php b/src/Infrastructure/Persistence/Stats/CachedStatsRepository.php index f79cef3f..ae24394f 100644 --- a/src/Infrastructure/Persistence/Stats/CachedStatsRepository.php +++ b/src/Infrastructure/Persistence/Stats/CachedStatsRepository.php @@ -6,6 +6,7 @@ use App\Domain\Stats\Stats; use App\Domain\Stats\StatsCollection; use App\Domain\Stats\StatsRepositoryInterface; +use DateTimeImmutable; use Psr\Cache\CacheItemPoolInterface; final class CachedStatsRepository implements StatsRepositoryInterface { @@ -30,7 +31,8 @@ public function create( int $favers = 0, int $totalDownloads = 0, int $monthlyDownloads = 0, - int $dailyDownloads = 0 + int $dailyDownloads = 0, + DateTimeImmutable $createdAt = new DateTimeImmutable() ): Stats { return $this->statsRepository->create( $packageName, @@ -42,7 +44,8 @@ public function create( $favers, $totalDownloads, $monthlyDownloads, - $dailyDownloads + $dailyDownloads, + $createdAt ); } diff --git a/src/Infrastructure/Persistence/Stats/PdoStatsRepository.php b/src/Infrastructure/Persistence/Stats/PdoStatsRepository.php index 21073c07..f8f641c7 100644 --- a/src/Infrastructure/Persistence/Stats/PdoStatsRepository.php +++ b/src/Infrastructure/Persistence/Stats/PdoStatsRepository.php @@ -61,7 +61,8 @@ public function create( int $favers = 0, int $totalDownloads = 0, int $monthlyDownloads = 0, - int $dailyDownloads = 0 + int $dailyDownloads = 0, + DateTimeImmutable $createdAt = new DateTimeImmutable() ): Stats { return $this->save( new Stats( @@ -75,22 +76,19 @@ public function create( $totalDownloads, $monthlyDownloads, $dailyDownloads, - new DateTimeImmutable() + $createdAt ) ); } public function all(): StatsCollection { - static $stmt = null; - if ($stmt === null) { - $stmt = $this->pdo->query( - <<pdo->query( + <<fetch(PDO::FETCH_ASSOC)) { diff --git a/src/Infrastructure/Persistence/Version/CachedVersionRepository.php b/src/Infrastructure/Persistence/Version/CachedVersionRepository.php index 07392d48..e4c5305b 100644 --- a/src/Infrastructure/Persistence/Version/CachedVersionRepository.php +++ b/src/Infrastructure/Persistence/Version/CachedVersionRepository.php @@ -7,6 +7,7 @@ use App\Domain\Version\VersionCollection; use App\Domain\Version\VersionRepositoryInterface; use App\Domain\Version\VersionStatusEnum; +use DateTimeImmutable; use Psr\Cache\CacheItemPoolInterface; final class CachedVersionRepository implements VersionRepositoryInterface { @@ -26,14 +27,16 @@ public function create( string $normalized, string $packageName, bool $release, - VersionStatusEnum $status = VersionStatusEnum::Unknown + VersionStatusEnum $status = VersionStatusEnum::Unknown, + DateTimeImmutable $createdAt = new DateTimeImmutable() ): Version { return $this->versionRepository->create( $number, $normalized, $packageName, $release, - $status + $status, + $createdAt ); } diff --git a/src/Infrastructure/Persistence/Version/PdoVersionRepository.php b/src/Infrastructure/Persistence/Version/PdoVersionRepository.php index ae6a27af..eb570a8b 100644 --- a/src/Infrastructure/Persistence/Version/PdoVersionRepository.php +++ b/src/Infrastructure/Persistence/Version/PdoVersionRepository.php @@ -49,7 +49,8 @@ public function create( string $normalized, string $packageName, bool $release, - VersionStatusEnum $status = VersionStatusEnum::Unknown + VersionStatusEnum $status = VersionStatusEnum::Unknown, + DateTimeImmutable $createdAt = new DateTimeImmutable() ): Version { return $this->save( new Version( @@ -59,13 +60,26 @@ public function create( $packageName, $release, $status, - new DateTimeImmutable() + $createdAt ) ); } public function all(): VersionCollection { - return new VersionCollection(); + $stmt = $this->pdo->query( + <<fetch(PDO::FETCH_ASSOC)) { + $versionCol->add($this->hydrate($row)); + } + + return $versionCol; } public function get(int $id): Version {