diff --git a/config/services.xml b/config/services.xml index 3f40e1f6..39e9acf9 100644 --- a/config/services.xml +++ b/config/services.xml @@ -87,25 +87,6 @@ - - - - - - - Doctrine\Website\Model\BlogPost - - - - - - - - - - Doctrine\Website\Model\BlogPost - - Doctrine\Website\Model\BlogPost @@ -140,22 +121,8 @@ - - - - - - Doctrine\Website\Model\Project - - - - - - - - - - Doctrine\Website\Model\Project + + Doctrine\Website\Model\Project @@ -168,11 +135,6 @@ Doctrine\Website\Model\Partner - - - Doctrine\Website\Model\Project - - @@ -210,5 +172,12 @@ + + + + + + diff --git a/config/services/orm.xml b/config/services/orm.xml index 02518a4b..23df7380 100644 --- a/config/services/orm.xml +++ b/config/services/orm.xml @@ -8,6 +8,8 @@ lib/Model + lib/Git + lib/Docs/RST @@ -24,6 +26,6 @@ - + diff --git a/lib/DataBuilder/ProjectDataBuilder.php b/lib/DataBuilder/ProjectDataBuilder.php deleted file mode 100644 index 1839d5bf..00000000 --- a/lib/DataBuilder/ProjectDataBuilder.php +++ /dev/null @@ -1,245 +0,0 @@ - 100])] -final readonly class ProjectDataBuilder implements DataBuilder -{ - public const DATA_FILE = 'projects'; - - private const DEFAULTS = [ - 'active' => true, - 'archived' => false, - 'integration' => false, - ]; - - public function __construct( - private ProjectDataRepository $projectDataRepository, - private ProjectGitSyncer $projectGitSyncer, - private ProjectDataReader $projectDataReader, - private ProjectVersionsReader $projectVersionsReader, - private RSTLanguagesDetector $rstLanguagesDetector, - private GetProjectPackagistData $getProjectPackagistData, - private string $projectsDir, - ) { - } - - public function getName(): string - { - return self::DATA_FILE; - } - - public function build(): WebsiteData - { - $repositoryNames = $this->projectDataRepository->getProjectRepositoryNames(); - - $projects = array_map(function (string $repositoryName): array { - return $this->buildProjectData($repositoryName); - }, $repositoryNames); - - return new WebsiteData(self::DATA_FILE, $projects); - } - - /** @return mixed[] */ - private function buildProjectData(string $repositoryName): array - { - // checkout master branch - $this->projectGitSyncer->checkoutDefaultBranch($repositoryName); - - $projectData = array_replace( - self::DEFAULTS, - $this->projectDataReader->read($repositoryName), - ); - - $projectData['versions'] = $this->buildProjectVersions( - $repositoryName, - $projectData, - ); - - $projectData['packagistData'] = $this->getProjectPackagistData->__invoke( - $projectData['composerPackageName'], - ); - - return $projectData; - } - - /** - * @param mixed[] $projectData - * - * @return mixed[] - */ - private function buildProjectVersions(string $repositoryName, array $projectData): array - { - $projectVersions = $this->readProjectVersionsFromGit($repositoryName); - - $this->applyConfiguredProjectVersions($projectVersions, $projectData); - - $this->sortProjectVersions($projectVersions); - - $this->prepareProjectVersions( - $repositoryName, - $projectVersions, - $projectData, - ); - - return $projectVersions; - } - - /** @return mixed[] */ - private function readProjectVersionsFromGit(string $repositoryName): array - { - $repositoryPath = $this->projectsDir . '/' . $repositoryName; - - $projectVersions = $this->projectVersionsReader->readProjectVersions($repositoryPath); - - // fix this, we shouldn't have null branch names at this point. Fix it further upstream - return array_filter($projectVersions, static function (array $projectVersion): bool { - return count($projectVersion['tags']) > 0; - }); - } - - /** - * @param mixed[] $projectVersions - * @param mixed[] $projectData - * - * @return mixed[] - */ - private function applyConfiguredProjectVersions( - array &$projectVersions, - array $projectData, - ): array { - foreach ($projectVersions as $key => $projectVersion) { - $configured = false; - - foreach ($projectData['versions'] as $k => $version) { - if ($this->containsSameProjectVersion($projectVersion, $version)) { - $version['tags'] = $projectVersion['tags']; - - $version['branchName'] = $projectVersion['branchName']; - - $projectVersions[$key] = $version; - - unset($projectData['versions'][$k]); - - $configured = true; - - break; - } - } - - if ($configured !== false) { - continue; - } - - $projectVersions[$key]['maintained'] = false; - } - - foreach ($projectData['versions'] as $projectVersion) { - $projectVersions[] = $projectVersion; - } - - return $projectVersions; - } - - /** - * @param mixed[] $a - * @param mixed[] $b - */ - private function containsSameProjectVersion(array $a, array $b): bool - { - if ($a['name'] === $b['name']) { - return true; - } - - if (! isset($b['branchName'])) { - return false; - } - - return $a['branchName'] === $b['branchName']; - } - - /** - * @param mixed[] $projectVersions - * @param mixed[] $projectData - * - * @return mixed[] - */ - private function prepareProjectVersions( - string $repositoryName, - array &$projectVersions, - array $projectData, - ): array { - $docsRepositoryName = $projectData['docsRepositoryName'] ?? $projectData['repositoryName']; - - $docsDir = $this->projectsDir . '/' . $docsRepositoryName . $projectData['docsPath']; - - foreach ($projectVersions as $key => $projectVersion) { - if (! isset($projectVersion['branchName'])) { - $this->projectGitSyncer->checkoutTag( - $docsRepositoryName, - end($projectVersion['tags'])->getName(), - ); - } else { - $this->projectGitSyncer->checkoutBranch( - $docsRepositoryName, - $projectVersion['branchName'], - ); - } - - $docsLanguages = array_map(static function (RSTLanguage $language): array { - return [ - 'code' => $language->getCode(), - 'path' => $language->getPath(), - ]; - }, $this->rstLanguagesDetector->detectLanguages($docsDir)); - - $projectVersions[$key]['hasDocs'] = count($docsLanguages) > 0; - $projectVersions[$key]['docsLanguages'] = $docsLanguages; - - if (! isset($projectVersion['tags'])) { - continue; - } - - $projectVersions[$key]['tags'] = array_map(static function (Tag $tag): array { - return [ - 'name' => $tag->getName(), - 'date' => $tag->getDate()->format('Y-m-d H:i:s'), - ]; - }, $projectVersion['tags']); - } - - // switch back to master - $this->projectGitSyncer->checkoutDefaultBranch($repositoryName); - - return $projectVersions; - } - - /** @param mixed[] $projectVersions */ - private function sortProjectVersions(array &$projectVersions): void - { - // sort by name so newest versions are first - usort($projectVersions, static function (array $a, array $b): int { - return strnatcmp($b['name'], $a['name']); - }); - } -} diff --git a/lib/DataSources/DbPrefill/Projects.php b/lib/DataSources/DbPrefill/Projects.php new file mode 100644 index 00000000..1a5e11b3 --- /dev/null +++ b/lib/DataSources/DbPrefill/Projects.php @@ -0,0 +1,120 @@ +dataSource->getSourceRows() as $sourceRow) { + $this->buildAndSaveProject($sourceRow); + } + } + + /** @param mixed[] $projectData */ + private function buildAndSaveProject(array $projectData): void + { + $active = (bool) ($projectData['active'] ?? true); + $archived = (bool) ($projectData['archived'] ?? false); + $name = (string) ($projectData['name'] ?? ''); + $shortName = (string) ($projectData['shortName'] ?? $name); + $slug = (string) ($projectData['slug'] ?? ''); + $docsSlug = (string) ($projectData['docsSlug'] ?? $slug); + $composerPackageName = (string) ($projectData['composerPackageName'] ?? ''); + $repositoryName = (string) ($projectData['repositoryName'] ?? ''); + $integration = (bool) ($projectData['integration'] ?? false); + $integrationFor = (string) ($projectData['integrationFor'] ?? ''); + $docsRepositoryName = (string) ($projectData['docsRepositoryName'] ?? $repositoryName); + $docsPath = (string) ($projectData['docsPath'] ?? '/docs'); + $codePath = (string) ($projectData['codePath'] ?? '/lib'); + $description = (string) ($projectData['description'] ?? ''); + $keywords = $projectData['keywords'] ?? []; + + $versions = new ArrayCollection(); + foreach ($projectData['versions'] ?? [] as $version) { + $projectVersion = new ProjectVersion($version); + + foreach ($version['tags'] ?? [] as $tag) { + $tag = new Tag($tag['name'], new DateTimeImmutable($tag['date'])); + $projectVersion->addTag($tag); + } + + $tagVersion = $projectVersion->getLatestTag()?->getName(); + if (isset($projectData['versionsGreaterThan']) && $tagVersion !== null && version_compare($projectData['versionsGreaterThan'], $tagVersion, '>')) { + continue; + } + + foreach ($version['docsLanguages'] ?? [] as $language) { + $rstLanguage = new RSTLanguage($language['code'], $language['path']); + $projectVersion->addDocsLanguage($rstLanguage); + } + + $this->entityManager->persist($projectVersion); + + $versions->add($projectVersion); + } + + $projectIntegrationType = null; + if ($integration) { + $projectIntegrationType = new ProjectIntegrationType(...$projectData['integrationType']); + + $this->entityManager->persist($projectIntegrationType); + } + + $projectStats = new ProjectStats( + (int) ($projectData['packagistData']['package']['github_stars'] ?? 0), + (int) ($projectData['packagistData']['package']['github_watchers'] ?? 0), + (int) ($projectData['packagistData']['package']['github_forks'] ?? 0), + (int) ($projectData['packagistData']['package']['github_open_issues'] ?? 0), + (int) ($projectData['packagistData']['package']['dependents'] ?? 0), + (int) ($projectData['packagistData']['package']['suggesters'] ?? 0), + (int) ($projectData['packagistData']['package']['downloads']['total'] ?? 0), + (int) ($projectData['packagistData']['package']['downloads']['monthly'] ?? 0), + (int) ($projectData['packagistData']['package']['downloads']['daily'] ?? 0), + ); + $this->entityManager->persist($projectStats); + + $project = new Project( + $projectStats, + $active, + $archived, + $name, + $shortName, + $slug, + $docsSlug, + $composerPackageName, + $repositoryName, + $integrationFor, + $docsRepositoryName, + $docsPath, + $codePath, + $description, + $projectIntegrationType, + $integration, + $keywords, + $versions, + ); + + $this->entityManager->persist($project); + $this->entityManager->flush(); + } +} diff --git a/lib/DataSources/Projects.php b/lib/DataSources/Projects.php index de5b0bd4..37c189ae 100644 --- a/lib/DataSources/Projects.php +++ b/lib/DataSources/Projects.php @@ -4,21 +4,232 @@ namespace Doctrine\Website\DataSources; -use Doctrine\Website\DataBuilder\ProjectDataBuilder; -use Doctrine\Website\DataBuilder\WebsiteDataReader; +use Doctrine\Website\Docs\RST\RSTLanguage; +use Doctrine\Website\Docs\RST\RSTLanguagesDetector; +use Doctrine\Website\Git\Tag; +use Doctrine\Website\Projects\GetProjectPackagistData; +use Doctrine\Website\Projects\ProjectDataReader; +use Doctrine\Website\Projects\ProjectDataRepository; +use Doctrine\Website\Projects\ProjectGitSyncer; +use Doctrine\Website\Projects\ProjectVersionsReader; + +use function array_filter; +use function array_map; +use function array_replace; +use function count; +use function end; +use function strnatcmp; +use function usort; final readonly class Projects implements DataSource { + private const DEFAULTS = [ + 'active' => true, + 'archived' => false, + 'integration' => false, + ]; + public function __construct( - private WebsiteDataReader $dataReader, + private ProjectDataRepository $projectDataRepository, + private ProjectGitSyncer $projectGitSyncer, + private ProjectDataReader $projectDataReader, + private ProjectVersionsReader $projectVersionsReader, + private RSTLanguagesDetector $rstLanguagesDetector, + private GetProjectPackagistData $getProjectPackagistData, + private string $projectsDir, ) { } /** @return mixed[][] */ public function getSourceRows(): array { - return $this->dataReader - ->read(ProjectDataBuilder::DATA_FILE) - ->getData(); + $repositoryNames = $this->projectDataRepository->getProjectRepositoryNames(); + + return array_map(function (string $repositoryName): array { + return $this->buildProjectData($repositoryName); + }, $repositoryNames); + } + + /** @return mixed[] */ + private function buildProjectData(string $repositoryName): array + { + // checkout master branch + $this->projectGitSyncer->checkoutDefaultBranch($repositoryName); + + $projectData = array_replace( + self::DEFAULTS, + $this->projectDataReader->read($repositoryName), + ); + + $projectData['versions'] = $this->buildProjectVersions( + $repositoryName, + $projectData, + ); + + $projectData['packagistData'] = $this->getProjectPackagistData->__invoke( + $projectData['composerPackageName'], + ); + + return $projectData; + } + + /** + * @param mixed[] $projectData + * + * @return mixed[] + */ + private function buildProjectVersions(string $repositoryName, array $projectData): array + { + $projectVersions = $this->readProjectVersionsFromGit($repositoryName); + + $this->applyConfiguredProjectVersions($projectVersions, $projectData); + + $this->sortProjectVersions($projectVersions); + + $this->prepareProjectVersions( + $repositoryName, + $projectVersions, + $projectData, + ); + + return $projectVersions; + } + + /** @return mixed[] */ + private function readProjectVersionsFromGit(string $repositoryName): array + { + $repositoryPath = $this->projectsDir . '/' . $repositoryName; + + $projectVersions = $this->projectVersionsReader->readProjectVersions($repositoryPath); + + // fix this, we shouldn't have null branch names at this point. Fix it further upstream + return array_filter($projectVersions, static function (array $projectVersion): bool { + return count($projectVersion['tags']) > 0; + }); + } + + /** + * @param mixed[] $projectVersions + * @param mixed[] $projectData + * + * @return mixed[] + */ + private function applyConfiguredProjectVersions( + array &$projectVersions, + array $projectData, + ): array { + foreach ($projectVersions as $key => $projectVersion) { + $configured = false; + + foreach ($projectData['versions'] as $k => $version) { + if ($this->containsSameProjectVersion($projectVersion, $version)) { + $version['tags'] = $projectVersion['tags']; + + $version['branchName'] = $projectVersion['branchName']; + + $projectVersions[$key] = $version; + + unset($projectData['versions'][$k]); + + $configured = true; + + break; + } + } + + if ($configured !== false) { + continue; + } + + $projectVersions[$key]['maintained'] = false; + } + + foreach ($projectData['versions'] as $projectVersion) { + $projectVersions[] = $projectVersion; + } + + return $projectVersions; + } + + /** + * @param mixed[] $a + * @param mixed[] $b + */ + private function containsSameProjectVersion(array $a, array $b): bool + { + if ($a['name'] === $b['name']) { + return true; + } + + if (! isset($b['branchName'])) { + return false; + } + + return $a['branchName'] === $b['branchName']; + } + + /** + * @param mixed[] $projectVersions + * @param mixed[] $projectData + * + * @return mixed[] + */ + private function prepareProjectVersions( + string $repositoryName, + array &$projectVersions, + array $projectData, + ): array { + $docsRepositoryName = $projectData['docsRepositoryName'] ?? $projectData['repositoryName']; + + $docsDir = $this->projectsDir . '/' . $docsRepositoryName . $projectData['docsPath']; + + foreach ($projectVersions as $key => $projectVersion) { + if (! isset($projectVersion['branchName'])) { + $this->projectGitSyncer->checkoutTag( + $docsRepositoryName, + end($projectVersion['tags'])->getName(), + ); + } else { + $this->projectGitSyncer->checkoutBranch( + $docsRepositoryName, + $projectVersion['branchName'], + ); + } + + $docsLanguages = array_map(static function (RSTLanguage $language): array { + return [ + 'code' => $language->getCode(), + 'path' => $language->getPath(), + ]; + }, $this->rstLanguagesDetector->detectLanguages($docsDir)); + + $projectVersions[$key]['hasDocs'] = count($docsLanguages) > 0; + $projectVersions[$key]['docsLanguages'] = $docsLanguages; + + if (! isset($projectVersion['tags'])) { + continue; + } + + $projectVersions[$key]['tags'] = array_map(static function (Tag $tag): array { + return [ + 'name' => $tag->getName(), + 'date' => $tag->getDate()->format('Y-m-d H:i:s'), + ]; + }, $projectVersion['tags']); + } + + // switch back to master + $this->projectGitSyncer->checkoutDefaultBranch($repositoryName); + + return $projectVersions; + } + + /** @param mixed[] $projectVersions */ + private function sortProjectVersions(array &$projectVersions): void + { + // sort by name so newest versions are first + usort($projectVersions, static function (array $a, array $b): int { + return strnatcmp($b['name'], $a['name']); + }); } } diff --git a/lib/Docs/RST/RSTLanguage.php b/lib/Docs/RST/RSTLanguage.php index 2e9e514b..c625a897 100644 --- a/lib/Docs/RST/RSTLanguage.php +++ b/lib/Docs/RST/RSTLanguage.php @@ -4,11 +4,25 @@ namespace Doctrine\Website\Docs\RST; -final readonly class RSTLanguage +use Doctrine\ORM\Mapping as ORM; +use Doctrine\Website\Model\ProjectVersion; + +#[ORM\Entity] +final class RSTLanguage { + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: 'integer')] + private int|null $id = null; + + #[ORM\ManyToOne(targetEntity: ProjectVersion::class, inversedBy: 'docsLanguages')] + private ProjectVersion $projectVersion; + public function __construct( - private string $code, - private string $path, + #[ORM\Column(type: 'string')] + private readonly string $code, + #[ORM\Column(type: 'string')] + private readonly string $path, ) { } @@ -21,4 +35,14 @@ public function getPath(): string { return $this->path; } + + public function setProjectVersion(ProjectVersion $version): void + { + $this->projectVersion = $version; + } + + public function getProjectVersion(): ProjectVersion + { + return $this->projectVersion; + } } diff --git a/lib/Git/Tag.php b/lib/Git/Tag.php index 9f47aa48..785fbf44 100644 --- a/lib/Git/Tag.php +++ b/lib/Git/Tag.php @@ -5,13 +5,16 @@ namespace Doctrine\Website\Git; use DateTimeImmutable; +use Doctrine\ORM\Mapping as ORM; +use Doctrine\Website\Model\ProjectVersion; use function ltrim; use function stripos; use function strpos; use function strtoupper; -final readonly class Tag +#[ORM\Entity] +final class Tag { private const ALPHA = 'alpha'; private const BETA = 'beta'; @@ -27,9 +30,19 @@ private const COMPOSER_EPOCH = '2011-09-25'; + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: 'integer')] + private int|null $id = null; + + #[ORM\ManyToOne(targetEntity: ProjectVersion::class, inversedBy: 'tags')] + private ProjectVersion $projectVersion; + public function __construct( - private string $name, - private DateTimeImmutable $date, + #[ORM\Column(type: 'string')] + private readonly string $name, + #[ORM\Column(type: 'datetime_immutable')] + private readonly DateTimeImmutable $date, ) { } @@ -87,4 +100,14 @@ public function getStability(): string return self::STABLE; } + + public function setProjectVersion(ProjectVersion $version): void + { + $this->projectVersion = $version; + } + + public function getProjectVersion(): ProjectVersion + { + return $this->projectVersion; + } } diff --git a/lib/Hydrators/ProjectHydrator.php b/lib/Hydrators/ProjectHydrator.php deleted file mode 100644 index 39e9d376..00000000 --- a/lib/Hydrators/ProjectHydrator.php +++ /dev/null @@ -1,99 +0,0 @@ - - */ -final class ProjectHydrator extends ModelHydrator -{ - /** @return class-string */ - protected function getClassName(): string - { - return Project::class; - } - - /** @param mixed[] $data */ - protected function doHydrate(array $data): void - { - $this->active = (bool) ($data['active'] ?? true); - $this->archived = (bool) ($data['archived'] ?? false); - $this->name = (string) ($data['name'] ?? ''); - $this->shortName = (string) ($data['shortName'] ?? $this->name); - $this->slug = (string) ($data['slug'] ?? ''); - $this->docsSlug = (string) ($data['docsSlug'] ?? $this->slug); - $this->composerPackageName = (string) ($data['composerPackageName'] ?? ''); - $this->repositoryName = (string) ($data['repositoryName'] ?? ''); - $this->isIntegration = (bool) ($data['integration'] ?? false); - $this->integrationFor = (string) ($data['integrationFor'] ?? ''); - $this->docsRepositoryName = (string) ($data['docsRepositoryName'] ?? $this->repositoryName); - $this->docsPath = (string) ($data['docsPath'] ?? '/docs'); - $this->codePath = (string) ($data['codePath'] ?? '/lib'); - $this->description = (string) ($data['description'] ?? ''); - $this->keywords = $data['keywords'] ?? []; - - if (! isset($data['versions'])) { - return; - } - - $versions = []; - - foreach ($data['versions'] as $version) { - $projectVersion = $version instanceof ProjectVersion - ? $version - : new ProjectVersion($version); - - $tagVersion = $projectVersion->getLatestTag()?->getName(); - if (isset($data['versionsGreaterThan']) && $tagVersion !== null && version_compare($data['versionsGreaterThan'], $tagVersion, '>')) { - continue; - } - - $versions[] = $projectVersion; - } - - $this->versions = $versions; - - if ($this->isIntegration) { - $this->projectIntegrationType = new ProjectIntegrationType($data['integrationType']); - } - - $this->projectStats = new ProjectStats( - (int) ($data['packagistData']['package']['github_stars'] ?? 0), - (int) ($data['packagistData']['package']['github_watchers'] ?? 0), - (int) ($data['packagistData']['package']['github_forks'] ?? 0), - (int) ($data['packagistData']['package']['github_open_issues'] ?? 0), - (int) ($data['packagistData']['package']['dependents'] ?? 0), - (int) ($data['packagistData']['package']['suggesters'] ?? 0), - (int) ($data['packagistData']['package']['downloads']['total'] ?? 0), - (int) ($data['packagistData']['package']['downloads']['monthly'] ?? 0), - (int) ($data['packagistData']['package']['downloads']['daily'] ?? 0), - ); - } -} diff --git a/lib/Model/Project.php b/lib/Model/Project.php index 1ccf2997..58d0d06a 100644 --- a/lib/Model/Project.php +++ b/lib/Model/Project.php @@ -5,57 +5,66 @@ namespace Doctrine\Website\Model; use Closure; -use Doctrine\SkeletonMapper\Mapping\ClassMetadataInterface; -use Doctrine\SkeletonMapper\Mapping\LoadMetadataInterface; +use Doctrine\Common\Collections\Collection; +use Doctrine\ORM\Mapping as ORM; +use Doctrine\Website\Repositories\ProjectRepository; use InvalidArgumentException; use function array_filter; use function array_values; use function sprintf; -class Project implements LoadMetadataInterface +#[ORM\Entity(repositoryClass: ProjectRepository::class)] +class Project { - private ProjectIntegrationType|null $projectIntegrationType = null; - - private ProjectStats $projectStats; - - private bool $active; - - private bool $archived; - - private string $name; - - private string $shortName; - - private string $slug; - - private string $docsSlug; - - private string $composerPackageName; - - private string $repositoryName; - - private bool $isIntegration = false; - - private string $integrationFor; - - private string $docsRepositoryName; - - private string $docsPath; - - private string $codePath; - - private string $description; - - /** @var string[] */ - private array $keywords = []; - - /** @var ProjectVersion[] */ - private array $versions = []; - - public static function loadMetadata(ClassMetadataInterface $metadata): void - { - $metadata->setIdentifier(['slug']); + /** + * @param Collection $versions + * @param string[] $keywords + */ + public function __construct( + #[ORM\OneToOne(targetEntity: ProjectStats::class, fetch: 'EAGER', orphanRemoval: true)] + #[ORM\JoinColumn(name: 'projectStats', referencedColumnName: 'id')] + private ProjectStats $projectStats, + #[ORM\Column(type: 'boolean')] + private bool $active, + #[ORM\Column(type: 'boolean')] + private bool $archived, + #[ORM\Column(type: 'string')] + private string $name, + #[ORM\Column(type: 'string')] + private string $shortName, + #[ORM\Id] + #[ORM\Column(type: 'string')] + private string $slug, + #[ORM\Column(type: 'string')] + private string $docsSlug, + #[ORM\Column(type: 'string')] + private string $composerPackageName, + #[ORM\Column(type: 'string')] + private string $repositoryName, + #[ORM\Column(type: 'string')] + private string $integrationFor, + #[ORM\Column(type: 'string')] + private string $docsRepositoryName, + #[ORM\Column(type: 'string')] + private string $docsPath, + #[ORM\Column(type: 'string')] + private string $codePath, + #[ORM\Column(type: 'string')] + private string $description, + #[ORM\OneToOne(targetEntity: ProjectIntegrationType::class, fetch: 'EAGER', orphanRemoval: true)] + #[ORM\JoinColumn(name: 'projectIntegrationType', referencedColumnName: 'id', nullable: true)] + private ProjectIntegrationType|null $projectIntegrationType, + #[ORM\Column(type: 'boolean')] + private bool $integration, + #[ORM\Column(type: 'simple_array')] + private array $keywords, + #[ORM\OneToMany(targetEntity: ProjectVersion::class, fetch: 'EAGER', mappedBy: 'project', orphanRemoval: true)] + private Collection $versions, + ) { + foreach ($this->versions as $version) { + $version->setProject($this); + } } public function getProjectIntegrationType(): ProjectIntegrationType|null @@ -110,7 +119,7 @@ public function getRepositoryName(): string public function isIntegration(): bool { - return $this->isIntegration; + return $this->integration; } public function getIntegrationFor(): string @@ -151,11 +160,13 @@ public function getKeywords(): array */ public function getVersions(Closure|null $filter = null): array { + $versions = $this->versions->getValues(); + if ($filter !== null) { - return array_values(array_filter($this->versions, $filter)); + return array_values(array_filter($versions, $filter)); } - return $this->versions; + return $versions; } /** @return ProjectVersion[] */ diff --git a/lib/Model/ProjectIntegrationType.php b/lib/Model/ProjectIntegrationType.php index 8472095f..25021d7a 100644 --- a/lib/Model/ProjectIntegrationType.php +++ b/lib/Model/ProjectIntegrationType.php @@ -4,20 +4,24 @@ namespace Doctrine\Website\Model; +use Doctrine\ORM\Mapping as ORM; + +#[ORM\Entity] class ProjectIntegrationType { - private string $name; - - private string $url; - - private string $icon; - - /** @param mixed[] $projectIntegrationType */ - public function __construct(array $projectIntegrationType) - { - $this->name = (string) ($projectIntegrationType['name'] ?? ''); - $this->url = (string) ($projectIntegrationType['url'] ?? ''); - $this->icon = (string) ($projectIntegrationType['icon'] ?? ''); + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: 'integer')] + private int|null $id = null; + + public function __construct( + #[ORM\Column(type: 'string')] + private string $name = '', + #[ORM\Column(type: 'string')] + private string $url = '', + #[ORM\Column(type: 'string')] + private string $icon = '', + ) { } public function getName(): string diff --git a/lib/Model/ProjectStats.php b/lib/Model/ProjectStats.php index b8a5a9e3..73154d42 100644 --- a/lib/Model/ProjectStats.php +++ b/lib/Model/ProjectStats.php @@ -4,17 +4,34 @@ namespace Doctrine\Website\Model; +use Doctrine\ORM\Mapping as ORM; + +#[ORM\Entity] final class ProjectStats { + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: 'integer')] + private int|null $id = null; + public function __construct( + #[ORM\Column(type: 'integer')] private int $githubStars = 0, + #[ORM\Column(type: 'integer')] private int $githubWatchers = 0, + #[ORM\Column(type: 'integer')] private int $githubForks = 0, + #[ORM\Column(type: 'integer')] private int $githubOpenIssues = 0, + #[ORM\Column(type: 'integer')] private int $dependents = 0, + #[ORM\Column(type: 'integer')] private int $suggesters = 0, + #[ORM\Column(type: 'integer')] private int $totalDownloads = 0, + #[ORM\Column(type: 'integer')] private int $monthlyDownloads = 0, + #[ORM\Column(type: 'integer')] private int $dailyDownloads = 0, ) { } diff --git a/lib/Model/ProjectVersion.php b/lib/Model/ProjectVersion.php index bd57a741..9a82fdd8 100644 --- a/lib/Model/ProjectVersion.php +++ b/lib/Model/ProjectVersion.php @@ -4,45 +4,65 @@ namespace Doctrine\Website\Model; -use DateTimeImmutable; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\ORM\Mapping as ORM; use Doctrine\Website\Docs\RST\RSTLanguage; use Doctrine\Website\Git\Tag; use InvalidArgumentException; -use function array_map; use function array_merge; use function count; -use function end; use function sprintf; +#[ORM\Entity] class ProjectVersion { private const UPCOMING = 'upcoming'; private const STABLE = 'stable'; private const UNMAINTAINED = 'unmaintained'; + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: 'integer')] + private int|null $id = null; + + #[ORM\ManyToOne(targetEntity: Project::class, inversedBy: 'versions')] + #[ORM\JoinColumn(referencedColumnName: 'slug')] + private Project $project; + + #[ORM\Column(type: 'string')] private string $name; + #[ORM\Column(type: 'string', nullable: true)] private string|null $branchName; + #[ORM\Column(type: 'string')] private string $slug; - private bool $current = false; + #[ORM\Column(type: 'boolean')] + private bool $current; - private bool $maintained = true; + #[ORM\Column(type: 'boolean')] + private bool $maintained; - private bool $upcoming = false; + #[ORM\Column(type: 'boolean')] + private bool $upcoming; - private bool $hasDocs = true; + #[ORM\Column(type: 'boolean')] + private bool $hasDocs; - /** @var RSTLanguage[] */ - private array $docsLanguages = []; + /** @var Collection */ + #[ORM\OneToMany(targetEntity: RSTLanguage::class, fetch: 'EAGER', mappedBy: 'projectVersion', orphanRemoval: true, cascade: ['persist'])] + private Collection $docsLanguages; /** @var string[] */ + #[ORM\Column(type: 'simple_array', nullable: true)] private array $aliases; - /** @var Tag[] */ - private array $tags; + /** @var Collection */ + #[ORM\OneToMany(targetEntity: Tag::class, fetch: 'EAGER', mappedBy: 'projectVersion', orphanRemoval: true, cascade: ['persist'])] + private Collection $tags; /** @param mixed[] $version */ public function __construct(array $version) @@ -54,16 +74,10 @@ public function __construct(array $version) $this->maintained = (bool) ($version['maintained'] ?? true); $this->upcoming = (bool) ($version['upcoming'] ?? false); $this->hasDocs = (bool) ($version['hasDocs'] ?? true); + $this->aliases = $version['aliases'] ?? []; - $this->docsLanguages = array_map(static function (array $language): RSTLanguage { - return new RSTLanguage($language['code'], $language['path']); - }, $version['docsLanguages'] ?? []); - - $this->tags = array_map(static function (array $tag): Tag { - return new Tag($tag['name'], new DateTimeImmutable($tag['date'])); - }, $version['tags'] ?? []); - - $this->aliases = $version['aliases'] ?? []; + $this->docsLanguages = new ArrayCollection(); + $this->tags = new ArrayCollection(); if (! $this->current) { return; @@ -124,10 +138,16 @@ public function hasDocs(): bool return $this->hasDocs; } + public function addDocsLanguage(RSTLanguage $docsLanguage): void + { + $docsLanguage->setProjectVersion($this); + $this->docsLanguages->add($docsLanguage); + } + /** @return RSTLanguage[] */ public function getDocsLanguages(): array { - return $this->docsLanguages; + return $this->docsLanguages->getValues(); } /** @return string[] */ @@ -136,10 +156,16 @@ public function getAliases(): array return $this->aliases; } + public function addTag(Tag $tag): void + { + $tag->setProjectVersion($this); + $this->tags->add($tag); + } + /** @return Tag[] */ public function getTags(): array { - return $this->tags; + return $this->tags->getValues(); } public function getTag(string $slug): Tag @@ -155,12 +181,12 @@ public function getTag(string $slug): Tag public function getFirstTag(): Tag|null { - return $this->tags[0] ?? null; + return $this->tags->first() ?: null; } public function getLatestTag(): Tag|null { - $latestTag = end($this->tags); + $latestTag = $this->tags->last(); if ($latestTag === false) { return null; @@ -214,4 +240,14 @@ public function getStabilityColor(string|null $stability = null): string return $map[$stability] ?? 'secondary'; } + + public function setProject(Project $project): void + { + $this->project = $project; + } + + public function getProject(): Project + { + return $this->project; + } } diff --git a/lib/Repositories/ProjectRepository.php b/lib/Repositories/ProjectRepository.php index 4c065f86..3c6d0f42 100644 --- a/lib/Repositories/ProjectRepository.php +++ b/lib/Repositories/ProjectRepository.php @@ -4,7 +4,7 @@ namespace Doctrine\Website\Repositories; -use Doctrine\SkeletonMapper\ObjectRepository\BasicObjectRepository; +use Doctrine\ORM\EntityRepository; use Doctrine\Website\Model\Project; use InvalidArgumentException; @@ -12,19 +12,10 @@ /** * @template T of Project - * @template-extends BasicObjectRepository + * @template-extends EntityRepository */ -class ProjectRepository extends BasicObjectRepository +class ProjectRepository extends EntityRepository { - /** @return Project[] */ - public function findAll(): array - { - /** @var Project[] $projects */ - $projects = parent::findAll(); - - return $projects; - } - public function findOneBySlug(string $slug): Project { $project = $this->findOneBy(['slug' => $slug]); diff --git a/lib/WebsiteBuilder.php b/lib/WebsiteBuilder.php index f8683f35..3c4fd62d 100644 --- a/lib/WebsiteBuilder.php +++ b/lib/WebsiteBuilder.php @@ -43,7 +43,6 @@ public function __construct( private readonly SourceFileRepository $sourceFileRepository, private readonly SourceFilesBuilder $sourceFilesBuilder, private readonly string $rootDir, - private readonly string $cacheDir, private readonly string $webpackBuildDir, ) { } @@ -72,8 +71,6 @@ public function build( $this->createProjectVersionAliases($buildDir); - $this->copyWebsiteBuildData($output, $buildDir); - $output->writeln(' - done'); } @@ -149,20 +146,6 @@ private function createProjectVersionAliases(string $buildDir): void } } - private function copyWebsiteBuildData(OutputInterface $output, string $buildDir): void - { - $from = $this->cacheDir . '/data'; - $to = $buildDir . '/website-data'; - - $output->writeln(sprintf( - ' - copying website build data from %s to %s.', - $from, - $to, - )); - - $this->filesystem->mirror($from, $to); - } - private function createDocsProjectVersionAlias( string $buildDir, Project $project, diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 2c10f7fe..6cac6405 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -13,7 +13,7 @@ parameters: message: "#While loop condition is always true#" path: "lib/Commands/BuildWebsiteCommand.php" count: 1 - - # Refactor class and remove this error + - # Will be removed once hydrators are completely gone message: "#is never written, only read#" path: "lib/Model/*.php" - # Temporary final classes in tests are covered by nunomaduro/mock-final-classes until the refactorings start @@ -23,5 +23,6 @@ parameters: message: "#contains unresolvable type#" path: "tests/" - - message: "#Parameter \\#4 \\$metadataFactory#" - path: "tests/Hydrators/Hydrators.php" + message: "#::\\$id is never read, only written#" + - # Will be removed once hydrators are completely gone + message: "/Parameter #4 \\$metadataFactory/" diff --git a/tests/DataBuilder/ProjectDataBuilderTest.php b/tests/DataBuilder/ProjectDataBuilderTest.php deleted file mode 100644 index 6a5d9d7f..00000000 --- a/tests/DataBuilder/ProjectDataBuilderTest.php +++ /dev/null @@ -1,212 +0,0 @@ -projectDataRepository->expects(self::once()) - ->method('getProjectRepositoryNames') - ->willReturn(['orm']); - - $this->projectGitSyncer->expects(self::any()) - ->method('checkoutDefaultBranch') - ->with('orm'); - - $this->projectDataReader->expects(self::once()) - ->method('read') - ->with('orm') - ->willReturn([ - 'composerPackageName' => 'doctrine/orm', - 'repositoryName' => 'orm', - 'docsPath' => '/docs', - 'versions' => [ - [ - 'name' => '1.0', - 'branchName' => null, - ], - [ - 'name' => '1.1', - 'branchName' => '1.1', - ], - ['name' => '1.2'], - ], - ]); - - $this->projectVersionsReader->expects(self::once()) - ->method('readProjectVersions') - ->with('/path/to/projects/orm') - ->willReturn([ - [ - 'name' => '1.0', - 'branchName' => null, - 'tags' => [ - new Tag('1.0.0', new DateTimeImmutable('2019-09-01')), - new Tag('1.0.1', new DateTimeImmutable('2019-09-02')), - ], - ], - [ - 'name' => '1.1', - 'branchName' => '1.1', - 'tags' => [ - new Tag('1.1.0', new DateTimeImmutable('2019-09-03')), - new Tag('1.1.1', new DateTimeImmutable('2019-09-04')), - ], - ], - [ - 'name' => '1.2', - 'branchName' => null, - 'tags' => [ - new Tag('1.2.0', new DateTimeImmutable('2019-09-05')), - ], - ], - ]); - - $this->projectGitSyncer->expects(self::once()) - ->method('checkoutBranch') - ->with('orm', '1.1'); - - $this->rstLanguagesDetector->expects(self::exactly(3)) - ->method('detectLanguages') - ->with('/path/to/projects/orm/docs') - ->willReturnOnConsecutiveCalls( - [ - new RSTLanguage('en', '/path/to/en'), - ], - [], - [], - ); - - $this->projectGitSyncer->expects(self::exactly(2)) - ->method('checkoutTag') - ->willReturnMap([ - ['orm', '1.0.1', null], - ['orm', '1.2.0', null], - ]); - - $this->getProjectPackagistData->expects(self::once()) - ->method('__invoke') - ->with('doctrine/orm') - ->willReturn(['package' => []]); - - $data = $this->projectDataBuilder->build()->getData(); - - $expected = [ - [ - 'active' => true, - 'archived' => false, - 'integration' => false, - 'composerPackageName' => 'doctrine/orm', - 'repositoryName' => 'orm', - 'docsPath' => '/docs', - 'versions' => [ - [ - 'name' => '1.2', - 'tags' => [ - [ - 'name' => '1.2.0', - 'date' => '2019-09-05 00:00:00', - ], - ], - 'branchName' => null, - 'hasDocs' => true, - 'docsLanguages' => [ - [ - 'code' => 'en', - 'path' => '/path/to/en', - ], - ], - ], - [ - 'name' => '1.1', - 'branchName' => '1.1', - 'tags' => [ - [ - 'name' => '1.1.0', - 'date' => '2019-09-03 00:00:00', - ], - [ - 'name' => '1.1.1', - 'date' => '2019-09-04 00:00:00', - ], - ], - 'hasDocs' => false, - 'docsLanguages' => [], - ], - [ - 'name' => '1.0', - 'branchName' => null, - 'tags' => [ - [ - 'name' => '1.0.0', - 'date' => '2019-09-01 00:00:00', - ], - [ - 'name' => '1.0.1', - 'date' => '2019-09-02 00:00:00', - ], - ], - 'hasDocs' => false, - 'docsLanguages' => [], - ], - ], - 'packagistData' => ['package' => []], - ], - ]; - - self::assertSame($expected, $data); - } - - protected function setUp(): void - { - $this->projectDataRepository = $this->createMock(ProjectDataRepository::class); - $this->projectGitSyncer = $this->createMock(ProjectGitSyncer::class); - $this->projectDataReader = $this->createMock(ProjectDataReader::class); - $this->projectVersionsReader = $this->createMock(ProjectVersionsReader::class); - $this->rstLanguagesDetector = $this->createMock(RSTLanguagesDetector::class); - $this->getProjectPackagistData = $this->createMock(GetProjectPackagistData::class); - $this->projectsDir = '/path/to/projects'; - - $this->projectDataBuilder = new ProjectDataBuilder( - $this->projectDataRepository, - $this->projectGitSyncer, - $this->projectDataReader, - $this->projectVersionsReader, - $this->rstLanguagesDetector, - $this->getProjectPackagistData, - $this->projectsDir, - ); - } -} diff --git a/tests/DataSources/DbPrefill/ProjectsTest.php b/tests/DataSources/DbPrefill/ProjectsTest.php new file mode 100644 index 00000000..bdda7f12 --- /dev/null +++ b/tests/DataSources/DbPrefill/ProjectsTest.php @@ -0,0 +1,148 @@ +getEntityManager(); + $repository = $entityManager->getRepository(Project::class); + $project = $repository->find('testproject'); + + assert($project instanceof Project); + + $entityManager->remove($project); + $entityManager->flush(); + } + + public function testPopulate(): void + { + $projectFixture = __DIR__ . '/fixtures/projects.json'; + $fixture = json_decode((string) file_get_contents($projectFixture), true); + + $entityManager = $this->getEntityManager(); + + $dataSource = $this->createMock(DataSource::class); + $dataSource->method('getSourceRows')->willReturn($fixture); + + $dbFill = new Projects($dataSource, $entityManager); + $dbFill->populate(); + + $this->assertProjectIsComplete($entityManager); + } + + private function assertProjectIsComplete(EntityManagerInterface $entityManager): void + { + $entityManager->clear(); + + $repository = $entityManager->getRepository(Project::class); + $project = $repository->find('testproject'); + + assert($project instanceof Project); + + self::assertSame('Testproject', $project->getName()); + self::assertSame('testproject', $project->getSlug()); + self::assertSame('Testproject', $project->getShortName()); + self::assertTrue($project->isActive()); + self::assertFalse($project->isArchived()); + self::assertSame('doctrine-testproject', $project->getDocsSlug()); + self::assertSame('testproject', $project->getDocsRepositoryName()); + self::assertSame('/docs', $project->getDocsPath()); + self::assertSame('/lib', $project->getCodePath()); + self::assertSame('doctrine/testproject', $project->getComposerPackageName()); + self::assertSame('testproject', $project->getRepositoryName()); + self::assertFalse($project->isIntegration()); + self::assertSame('', $project->getIntegrationFor()); + self::assertSame('It\'s a testproject', $project->getDescription()); + self::assertSame(['testproject', 'docblock', 'parser'], $project->getKeywords()); + $this->assertProjectStats($project->getProjectStats()); + + $versions = $project->getVersions(); + self::assertCount(1, $versions); + $this->assertVersion($versions[0]); + } + + private function assertVersion(ProjectVersion $version): void + { + self::assertSame('2.0', $version->getName()); + self::assertSame('2.0.x', $version->getBranchName()); + self::assertSame('2.0', $version->getSlug()); + self::assertTrue($version->isCurrent()); + self::assertTrue($version->isMaintained()); + self::assertTrue($version->hasDocs()); + self::assertSame(['foo', 'current', 'stable'], $version->getAliases()); + + self::assertCount(1, $version->getTags()); + $this->assertTag($version->getTag('2.0.0')); + + $docsLanguages = $version->getDocsLanguages(); + self::assertCount(1, $docsLanguages); + $this->assertDocsLanguage($docsLanguages[0]); + } + + private function assertTag(Tag $tag): void + { + self::assertSame('2.0.0', $tag->getName()); + self::assertSame('2.0.0', $tag->getDisplayName()); + self::assertSame('2.0.0', $tag->getSlug()); + self::assertSame(1671492263, $tag->getDate()->getTimestamp()); + } + + private function assertDocsLanguage(RSTLanguage $docsLanguage): void + { + self::assertSame('en', $docsLanguage->getCode()); + self::assertSame('projects/testproject/docs/en', $docsLanguage->getPath()); + } + + private function assertProjectStats(ProjectStats $projectStats): void + { + self::assertSame(6729, $projectStats->getGithubStars()); + self::assertSame(41, $projectStats->getGithubWatchers()); + self::assertSame(236, $projectStats->getGithubForks()); + self::assertSame(30, $projectStats->getGithubOpenIssues()); + self::assertSame(2349, $projectStats->getDependents()); + self::assertSame(74, $projectStats->getSuggesters()); + self::assertSame(426454499, $projectStats->getTotalDownloads()); + self::assertSame(6487217, $projectStats->getMonthlyDownloads()); + self::assertSame(260080, $projectStats->getDailyDownloads()); + } + + private function getEntityManager(): EntityManagerInterface + { + $entityManager = $this->getContainer()->get(EntityManagerInterface::class); + assert($entityManager instanceof EntityManagerInterface); + + return $entityManager; + } +} diff --git a/tests/DataSources/DbPrefill/fixtures/projects.json b/tests/DataSources/DbPrefill/fixtures/projects.json new file mode 100644 index 00000000..2f4616d0 --- /dev/null +++ b/tests/DataSources/DbPrefill/fixtures/projects.json @@ -0,0 +1,201 @@ +[ + { + "active": true, + "archived": false, + "integration": false, + "name": "Testproject", + "repositoryName": "testproject", + "docsPath": "/docs", + "codePath": "/lib", + "slug": "testproject", + "versionsGreaterThan": "1.0.1", + "versions": [ + { + "name": "2.0", + "branchName": "2.0.x", + "aliases": [ + "foo" + ], + "current": true, + "maintained": true, + "tags": [ + { + "name": "2.0.0", + "date": "2022-12-19 18:24:23" + } + ], + "hasDocs": true, + "docsLanguages": [ + { + "code": "en", + "path": "projects/testproject/docs/en" + } + ] + }, + { + "name": "1.0", + "slug": "1.0", + "branchName": null, + "tags": [ + { + "name": "v1.0", + "date": "2013-01-12 20:26:03" + } + ], + "maintained": false, + "hasDocs": false, + "docsLanguages": [] + } + ], + "composerPackageName": "doctrine/testproject", + "description": "It's a testproject", + "keywords": [ + "testproject", + "docblock", + "parser" + ], + "docsSlug": "doctrine-testproject", + "packagistData": { + "package": { + "name": "doctrine/testproject", + "description": "It's a testproject", + "time": "2013-01-12T19:24:37+00:00", + "maintainers": [ + { + "name": "beberlei", + "avatar_url": "https://www.gravatar.com/avatar/75f5fb3ddda052e46f1daed314ae69ab?d=identicon" + } + ], + "versions": { + "2.0.0": { + "name": "doctrine/testproject", + "description": "It's a testproject", + "keywords": [ + "testproject", + "parser", + "docblock" + ], + "homepage": "https://www.doctrine-project.org/projects/testproject.html", + "version": "2.0.0", + "version_normalized": "2.0.0.0", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Doe", + "email": "john@doe.dev", + "role": "Creator" + } + ], + "source": { + "url": "https://github.com/doctrine/testproject.git", + "type": "git", + "reference": "d02c9f3742044e17d5fa8d28d8402a2d95c33302" + }, + "dist": { + "url": "https://api.github.com/repos/doctrine/testproject/zipball/d02c9f3742044e17d5fa8d28d8402a2d95c33302", + "type": "zip", + "shasum": "", + "reference": "d02c9f3742044e17d5fa8d28d8402a2d95c33302" + }, + "type": "library", + "support": { + "issues": "https://github.com/doctrine/testproject/issues", + "source": "https://github.com/doctrine/testproject/tree/2.0.0" + }, + "funding": [], + "time": "2022-12-19T18:17:20+00:00", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Testproject\\": "lib/Doctrine/Common/Testproject" + } + }, + "require": { + "php": "^7.2 || ^8.0", + "ext-tokenizer": "*", + "doctrine/lexer": "^2 || ^3", + "psr/cache": "^1 || ^2 || ^3" + }, + "require-dev": { + "doctrine/cache": "^2.0", + "doctrine/coding-standard": "^10", + "phpstan/phpstan": "^1.8.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "symfony/cache": "^5.4 || ^6", + "vimeo/psalm": "^4.10" + }, + "suggest": { + "php": "PHP 8.0 or higher comes with attributes, a native replacement for testproject" + } + }, + "v1.0": { + "name": "doctrine/testproject", + "description": "It's a testproject", + "keywords": [ + "testproject", + "parser", + "docblock" + ], + "homepage": "http://www.doctrine-project.org", + "version": "v1.0", + "version_normalized": "1.0.0.0", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Doe", + "email": "john@doe.dev", + "role": "Creator" + } + ], + "source": { + "url": "https://github.com/doctrine/testproject.git", + "type": "git", + "reference": "fae359b3efd908e407a0105ff8956b5c94ddca8e" + }, + "dist": { + "url": "https://api.github.com/repos/doctrine/testproject/zipball/fae359b3efd908e407a0105ff8956b5c94ddca8e", + "type": "zip", + "shasum": "", + "reference": "fae359b3efd908e407a0105ff8956b5c94ddca8e" + }, + "type": "library", + "support": { + "source": "https://github.com/doctrine/testproject/tree/v1.0" + }, + "time": "2013-01-12T19:23:32+00:00", + "autoload": { + "psr-0": { + "Doctrine\\Common\\Testproject\\": "lib/" + } + }, + "require": { + "php": ">=5.3.2", + "doctrine/lexer": "1.*" + }, + "require-dev": { + "doctrine/cache": "1.*" + } + } + }, + "type": "library", + "repository": "https://github.com/doctrine/testproject", + "github_stars": 6729, + "github_watchers": 41, + "github_forks": 236, + "github_open_issues": 30, + "language": "PHP", + "dependents": 2349, + "suggesters": 74, + "downloads": { + "total": 426454499, + "monthly": 6487217, + "daily": 260080 + }, + "favers": 6768 + } + } + } +] diff --git a/tests/DataSources/ProjectsTest.php b/tests/DataSources/ProjectsTest.php index 00ed1928..b2b11572 100644 --- a/tests/DataSources/ProjectsTest.php +++ b/tests/DataSources/ProjectsTest.php @@ -4,56 +4,209 @@ namespace Doctrine\Website\Tests\DataSources; -use Doctrine\Website\DataBuilder\ProjectDataBuilder; -use Doctrine\Website\DataBuilder\WebsiteData; -use Doctrine\Website\DataBuilder\WebsiteDataReader; +use DateTimeImmutable; use Doctrine\Website\DataSources\Projects; +use Doctrine\Website\Docs\RST\RSTLanguage; +use Doctrine\Website\Docs\RST\RSTLanguagesDetector; +use Doctrine\Website\Git\Tag; +use Doctrine\Website\Projects\GetProjectPackagistData; +use Doctrine\Website\Projects\ProjectDataReader; +use Doctrine\Website\Projects\ProjectDataRepository; +use Doctrine\Website\Projects\ProjectGitSyncer; +use Doctrine\Website\Projects\ProjectVersionsReader; use Doctrine\Website\Tests\TestCase; use PHPUnit\Framework\MockObject\MockObject; class ProjectsTest extends TestCase { - private WebsiteDataReader&MockObject $dataReader; + private ProjectDataRepository&MockObject $projectDataRepository; - private Projects $projects; + private ProjectGitSyncer&MockObject $projectGitSyncer; - protected function setUp(): void - { - $this->dataReader = $this->createMock(WebsiteDataReader::class); + private ProjectDataReader&MockObject $projectDataReader; - $this->projects = new Projects( - $this->dataReader, - ); - } + private ProjectVersionsReader&MockObject $projectVersionsReader; + + private RSTLanguagesDetector&MockObject $rstLanguagesDetector; + + private GetProjectPackagistData&MockObject $getProjectPackagistData; + + private string $projectsDir; - public function testGetSourceRows(): void + private Projects $dataSource; + + public function testBuild(): void { + $this->projectDataRepository->expects(self::once()) + ->method('getProjectRepositoryNames') + ->willReturn(['orm']); + + $this->projectGitSyncer->expects(self::exactly(2)) + ->method('checkoutDefaultBranch') + ->with('orm'); + + $this->projectDataReader->expects(self::once()) + ->method('read') + ->with('orm') + ->willReturn([ + 'composerPackageName' => 'doctrine/orm', + 'repositoryName' => 'orm', + 'docsPath' => '/docs', + 'versions' => [ + [ + 'name' => '1.0', + 'branchName' => null, + ], + [ + 'name' => '1.1', + 'branchName' => '1.1', + ], + ['name' => '1.2'], + ], + ]); + + $this->projectVersionsReader->expects(self::once()) + ->method('readProjectVersions') + ->with('/path/to/projects/orm') + ->willReturn([ + [ + 'name' => '1.0', + 'branchName' => null, + 'tags' => [ + new Tag('1.0.0', new DateTimeImmutable('2019-09-01')), + new Tag('1.0.1', new DateTimeImmutable('2019-09-02')), + ], + ], + [ + 'name' => '1.1', + 'branchName' => '1.1', + 'tags' => [ + new Tag('1.1.0', new DateTimeImmutable('2019-09-03')), + new Tag('1.1.1', new DateTimeImmutable('2019-09-04')), + ], + ], + [ + 'name' => '1.2', + 'branchName' => null, + 'tags' => [ + new Tag('1.2.0', new DateTimeImmutable('2019-09-05')), + ], + ], + ]); + + $this->projectGitSyncer->expects(self::once()) + ->method('checkoutBranch') + ->with('orm', '1.1'); + + $this->rstLanguagesDetector->expects(self::exactly(3)) + ->method('detectLanguages') + ->with('/path/to/projects/orm/docs') + ->willReturnOnConsecutiveCalls( + [ + new RSTLanguage('en', '/path/to/en'), + ], + [], + [], + ); + + $this->projectGitSyncer->expects(self::exactly(2)) + ->method('checkoutTag') + ->willReturnMap([ + ['orm', '1.0.1', null], + ['orm', '1.2.0', null], + ]); + + $this->getProjectPackagistData->expects(self::once()) + ->method('__invoke') + ->with('doctrine/orm') + ->willReturn(['package' => []]); + + $data = $this->dataSource->getSourceRows(); + $expected = [ [ 'active' => true, 'archived' => false, 'integration' => false, - 'name' => 'Object Relational Mapper', - 'repositoryName' => 'doctrine2', - 'versions' => [], - ], - [ - 'active' => true, - 'archived' => false, - 'integration' => false, - 'name' => 'Database Abstraction Layer', - 'repositoryName' => 'dbal', - 'versions' => [], + 'composerPackageName' => 'doctrine/orm', + 'repositoryName' => 'orm', + 'docsPath' => '/docs', + 'versions' => [ + [ + 'name' => '1.2', + 'tags' => [ + [ + 'name' => '1.2.0', + 'date' => '2019-09-05 00:00:00', + ], + ], + 'branchName' => null, + 'hasDocs' => true, + 'docsLanguages' => [ + [ + 'code' => 'en', + 'path' => '/path/to/en', + ], + ], + ], + [ + 'name' => '1.1', + 'branchName' => '1.1', + 'tags' => [ + [ + 'name' => '1.1.0', + 'date' => '2019-09-03 00:00:00', + ], + [ + 'name' => '1.1.1', + 'date' => '2019-09-04 00:00:00', + ], + ], + 'hasDocs' => false, + 'docsLanguages' => [], + ], + [ + 'name' => '1.0', + 'branchName' => null, + 'tags' => [ + [ + 'name' => '1.0.0', + 'date' => '2019-09-01 00:00:00', + ], + [ + 'name' => '1.0.1', + 'date' => '2019-09-02 00:00:00', + ], + ], + 'hasDocs' => false, + 'docsLanguages' => [], + ], + ], + 'packagistData' => ['package' => []], ], ]; - $this->dataReader->expects(self::once()) - ->method('read') - ->with(ProjectDataBuilder::DATA_FILE) - ->willReturn(new WebsiteData('test', $expected)); + self::assertSame($expected, $data); + } - $projectRows = $this->projects->getSourceRows(); + protected function setUp(): void + { + $this->projectDataRepository = $this->createMock(ProjectDataRepository::class); + $this->projectGitSyncer = $this->createMock(ProjectGitSyncer::class); + $this->projectDataReader = $this->createMock(ProjectDataReader::class); + $this->projectVersionsReader = $this->createMock(ProjectVersionsReader::class); + $this->rstLanguagesDetector = $this->createMock(RSTLanguagesDetector::class); + $this->getProjectPackagistData = $this->createMock(GetProjectPackagistData::class); + $this->projectsDir = '/path/to/projects'; - self::assertSame($expected, $projectRows); + $this->dataSource = new Projects( + $this->projectDataRepository, + $this->projectGitSyncer, + $this->projectDataReader, + $this->projectVersionsReader, + $this->rstLanguagesDetector, + $this->getProjectPackagistData, + $this->projectsDir, + ); } } diff --git a/tests/Docs/BuildDocsTest.php b/tests/Docs/BuildDocsTest.php index 5078d65c..59bdf03e 100644 --- a/tests/Docs/BuildDocsTest.php +++ b/tests/Docs/BuildDocsTest.php @@ -4,9 +4,12 @@ namespace Doctrine\Website\Tests\Docs; +use DateTimeImmutable; use Doctrine\Website\Docs\BuildDocs; use Doctrine\Website\Docs\RST\RSTBuilder; +use Doctrine\Website\Docs\RST\RSTLanguage; use Doctrine\Website\Docs\SearchIndexer; +use Doctrine\Website\Git\Tag; use Doctrine\Website\Model\Project; use Doctrine\Website\Model\ProjectVersion; use Doctrine\Website\Projects\ProjectGitSyncer; @@ -48,15 +51,8 @@ public function testBuildWithBranchCheckout(): void { $output = $this->createMock(OutputInterface::class); - $version = new ProjectVersion([ - 'branchName' => '1.0', - 'docsLanguages' => [ - [ - 'code' => 'en', - 'path' => '/en', - ], - ], - ]); + $version = new ProjectVersion(['branchName' => '1.0']); + $version->addDocsLanguage(new RSTLanguage('en', '/en')); $repositoryName = 'test-project'; @@ -111,6 +107,8 @@ public function testBuildWithTagCheckout(): void ], ], ]); + $version->addDocsLanguage(new RSTLanguage('en', '/en')); + $version->addTag(new Tag('1.0.1', new DateTimeImmutable('2000-01-01'))); $repositoryName = 'test-project'; @@ -152,14 +150,8 @@ public function testBuildWithInvalidProjectVersion(): void { $output = $this->createMock(OutputInterface::class); - $version = new ProjectVersion([ - 'docsLanguages' => [ - [ - 'code' => 'en', - 'path' => '/en', - ], - ], - ]); + $version = new ProjectVersion([]); + $version->addDocsLanguage(new RSTLanguage('en', '/en')); $repositoryName = 'test-project'; diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index 24988c6c..31496075 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -35,7 +35,7 @@ protected function setUp(): void return; } - self::markTestSkipped('This test requires ./bin/console build-website to have been ran.'); + self::markTestSkipped('This test requires ./bin/console build-website to have been run.'); } public function testProjectVersionsAndTags(): void diff --git a/tests/Hydrators/ProjectHydratorTest.php b/tests/Hydrators/ProjectHydratorTest.php deleted file mode 100644 index c1192443..00000000 --- a/tests/Hydrators/ProjectHydratorTest.php +++ /dev/null @@ -1,182 +0,0 @@ -createHydrator(ProjectHydrator::class); - $propertyValues = [ - 'active' => false, - 'archived' => true, - 'name' => 'name', - 'shortName' => 'shortName', - 'slug' => 'slug', - 'docsSlug' => 'docsSlug', - 'composerPackageName' => 'composerPackageName', - 'repositoryName' => 'repositoryName', - 'integration' => true, - 'integrationFor' => 'integrationFor', - 'docsRepositoryName' => 'docsRepositoryName', - 'docsPath' => 'docsPath', - 'codePath' => 'codePath', - 'description' => 'description', - 'keywords' => ['keywords'], - 'versions' => [ - ['name' => 'v1'], - new ProjectVersion(['name' => 'v2']), - ], - 'integrationType' => [], - 'packagistData' => [ - 'package' => [ - 'github_stars' => 1, - 'github_watchers' => 2, - 'github_forks' => 3, - 'github_open_issues' => 4, - 'dependents' => 5, - 'suggesters' => 6, - 'downloads' => [ - 'total' => 7, - 'monthly' => 8, - 'daily' => 9, - ], - ], - ], - ]; - - $expected = new Project(); - $this->populate($expected, [ - 'active' => false, - 'archived' => true, - 'name' => 'name', - 'shortName' => 'shortName', - 'slug' => 'slug', - 'docsSlug' => 'docsSlug', - 'composerPackageName' => 'composerPackageName', - 'repositoryName' => 'repositoryName', - 'isIntegration' => true, - 'integrationFor' => 'integrationFor', - 'docsRepositoryName' => 'docsRepositoryName', - 'docsPath' => 'docsPath', - 'codePath' => 'codePath', - 'description' => 'description', - 'keywords' => ['keywords'], - 'versions' => [ - new ProjectVersion(['name' => 'v1']), - new ProjectVersion(['name' => 'v2']), - ], - 'projectIntegrationType' => new ProjectIntegrationType([]), - 'projectStats' => new ProjectStats(1, 2, 3, 4, 5, 6, 7, 8, 9), - ]); - - $project = new Project(); - - $hydrator->hydrate($project, $propertyValues); - - self::assertEquals($expected, $project); - } - - public function testHydrateDefaultValues(): void - { - $hydrator = $this->createHydrator(ProjectHydrator::class); - $propertyValues = [ - 'name' => 'name', - 'slug' => 'slug', - 'repositoryName' => 'repositoryName', - 'versions' => [], - ]; - - $expected = new Project(); - $this->populate($expected, [ - 'active' => true, - 'archived' => false, - 'name' => 'name', - 'shortName' => 'name', - 'slug' => 'slug', - 'docsSlug' => 'slug', - 'composerPackageName' => '', - 'repositoryName' => 'repositoryName', - 'isIntegration' => false, - 'integrationFor' => '', - 'docsRepositoryName' => 'repositoryName', - 'docsPath' => '/docs', - 'codePath' => '/lib', - 'description' => '', - 'keywords' => [], - 'versions' => [], - 'projectIntegrationType' => null, - 'projectStats' => new ProjectStats(0, 0, 0, 0, 0, 0, 0, 0, 0), - ]); - - $project = new Project(); - - $hydrator->hydrate($project, $propertyValues); - - self::assertEquals($expected, $project); - } - - public function testHydrateNoVersions(): void - { - $hydrator = $this->createHydrator(ProjectHydrator::class); - $propertyValues = [ - 'name' => 'name', - 'slug' => 'slug', - 'repositoryName' => 'repositoryName', - ]; - - $expected = new Project(); - $this->populate($expected, [ - 'active' => true, - 'archived' => false, - 'name' => 'name', - 'shortName' => 'name', - 'slug' => 'slug', - 'docsSlug' => 'slug', - 'composerPackageName' => '', - 'repositoryName' => 'repositoryName', - 'isIntegration' => false, - 'integrationFor' => '', - 'docsRepositoryName' => 'repositoryName', - 'docsPath' => '/docs', - 'codePath' => '/lib', - 'description' => '', - ]); - - $project = new Project(); - - $hydrator->hydrate($project, $propertyValues); - - self::assertEquals($expected, $project); - } - - public function testHydrateWithFilter(): void - { - $hydrator = $this->createHydrator(ProjectHydrator::class); - $propertyValues = [ - 'name' => 'name', - 'slug' => 'slug', - 'repositoryName' => 'repositoryName', - 'versionsGreaterThan' => '1.99.0', - 'versions' => [ - ['name' => '1.0.0', 'tags' => [['name' => '1.0.0', 'date' => '2024-10-10']]], - new ProjectVersion(['name' => '2.0.0', 'tags' => [['name' => '2.0.0', 'date' => '2024-10-10']]]), - ], - ]; - - $project = new Project(); - - $hydrator->hydrate($project, $propertyValues); - - self::assertCount(1, $project->getVersions()); - self::assertEquals('2.0.0', $project->getVersions()[0]->getLatestTag()?->getName()); - } -} diff --git a/tests/Projects/GetTotalDownloadsTest.php b/tests/Projects/GetTotalDownloadsTest.php index 2a3a7b7c..a486cce8 100644 --- a/tests/Projects/GetTotalDownloadsTest.php +++ b/tests/Projects/GetTotalDownloadsTest.php @@ -4,6 +4,7 @@ namespace Doctrine\Website\Tests\Projects; +use Doctrine\Website\Model\ProjectStats; use Doctrine\Website\Projects\GetTotalDownloads; use Doctrine\Website\Repositories\ProjectRepository; use Doctrine\Website\Tests\TestCase; @@ -29,12 +30,7 @@ public function testGetTotalDownloadsTest(): void private function createProjectData(int $totalDownloads): array { return [ - 'packagistData' => [ - 'package' => [ - 'downloads' => ['total' => $totalDownloads], - ], - ], - 'versions' => [], + 'projectStats' => new ProjectStats(totalDownloads: $totalDownloads), ]; } } diff --git a/tests/Projects/ProjectTest.php b/tests/Projects/ProjectTest.php index 7cee9ca7..b91250db 100644 --- a/tests/Projects/ProjectTest.php +++ b/tests/Projects/ProjectTest.php @@ -4,6 +4,7 @@ namespace Doctrine\Website\Tests\Projects; +use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Website\Model\Project; use Doctrine\Website\Model\ProjectVersion; use Doctrine\Website\Tests\TestCase; @@ -25,29 +26,29 @@ protected function setUp(): void 'composerPackageName' => 'doctrine/test-project', 'repositoryName' => 'test-project', 'docsRepositoryName' => 'test-project', - 'docsDir' => '/docs', + 'docsPath' => '/docs', 'codePath' => '/src', 'description' => 'Test description.', 'keywords' => ['keyword1', 'keyword2'], - 'versions' => [ - [ + 'versions' => new ArrayCollection([ + new ProjectVersion([ 'name' => 'master', 'branchName' => 'master', 'slug' => 'latest', - ], - [ + ]), + new ProjectVersion([ 'name' => '2.0', 'branchName' => '2.0', 'slug' => '2.0', 'current' => true, - ], - [ + ]), + new ProjectVersion([ 'name' => '1.0', 'branchName' => '1.0', 'slug' => '1.0', 'maintained' => false, - ], - ], + ]), + ]), ]); } diff --git a/tests/Projects/ProjectVersionTest.php b/tests/Projects/ProjectVersionTest.php index 3bb06a2b..755b22c8 100644 --- a/tests/Projects/ProjectVersionTest.php +++ b/tests/Projects/ProjectVersionTest.php @@ -4,6 +4,8 @@ namespace Doctrine\Website\Tests\Projects; +use DateTimeImmutable; +use Doctrine\Website\Git\Tag; use Doctrine\Website\Model\ProjectVersion; use Doctrine\Website\Tests\TestCase; @@ -85,14 +87,8 @@ public function testHasTags(): void self::assertFalse($projectVersion->hasTags()); - $projectVersion = new ProjectVersion([ - 'tags' => [ - [ - 'name' => '1.0', - 'date' => '2000-01-01', - ], - ], - ]); + $projectVersion = new ProjectVersion([]); + $projectVersion->addTag(new Tag('1.0', new DateTimeImmutable('2000-01-01'))); self::assertTrue($projectVersion->hasTags()); } diff --git a/tests/Requests/ProjectVersionRequestsTest.php b/tests/Requests/ProjectVersionRequestsTest.php index e939cf6a..e39bbda5 100644 --- a/tests/Requests/ProjectVersionRequestsTest.php +++ b/tests/Requests/ProjectVersionRequestsTest.php @@ -4,7 +4,9 @@ namespace Doctrine\Website\Tests\Requests; +use Doctrine\Common\Collections\ArrayCollection; use Doctrine\StaticWebsiteGenerator\Request\ArrayRequestCollection; +use Doctrine\Website\Model\ProjectVersion; use Doctrine\Website\Repositories\ProjectRepository; use Doctrine\Website\Requests\ProjectVersionRequests; use Doctrine\Website\Tests\TestCase; @@ -13,12 +15,12 @@ class ProjectVersionRequestsTest extends TestCase { public function testGetProjectVersions(): void { - $partner = $this->createModel(ProjectRepository::class, [ + $partner = $this->createProject([ 'slug' => 'project', - 'versions' => [ - ['slug' => 'v1'], - ['slug' => 'v2'], - ], + 'versions' => new ArrayCollection([ + new ProjectVersion(['slug' => 'v1']), + new ProjectVersion(['slug' => 'v2']), + ]), ]); $projectRepository = $this->createMock(ProjectRepository::class); $projectRepository->expects(self::once()) diff --git a/tests/TestCase.php b/tests/TestCase.php index a70b0f14..50199c1e 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -4,13 +4,16 @@ namespace Doctrine\Website\Tests; +use Doctrine\Common\Collections\ArrayCollection; use Doctrine\SkeletonMapper\ObjectRepository\ObjectRepositoryInterface; use Doctrine\Website\Application; use Doctrine\Website\Model\Project; -use Doctrine\Website\Repositories\ProjectRepository; +use Doctrine\Website\Model\ProjectStats; use PHPUnit\Framework\TestCase as BaseTestCase; use Symfony\Component\DependencyInjection\ContainerBuilder; +use function array_merge; + abstract class TestCase extends BaseTestCase { private static ContainerBuilder|null $container = null; @@ -40,9 +43,29 @@ protected function createModel(string $repositoryClassName, array $data): object /** @param mixed[] $data */ protected function createProject(array $data): Project { - $project = $this->createModel(ProjectRepository::class, $data); - self::assertInstanceOf(Project::class, $project); + $default = [ + 'projectStats' => new ProjectStats(), + 'active' => true, + 'archived' => false, + 'name' => '', + 'shortName' => '', + 'slug' => '', + 'docsSlug' => '', + 'composerPackageName' => '', + 'repositoryName' => '', + 'integrationFor' => '', + 'docsRepositoryName' => '', + 'docsPath' => '', + 'codePath' => '', + 'description' => '', + 'projectIntegrationType' => null, + 'integration' => true, + 'keywords' => [], + 'versions' => new ArrayCollection(), + ]; + + $data = array_merge($default, $data); - return $project; + return new Project(...$data); } } diff --git a/tests/Twig/MainExtensionTest.php b/tests/Twig/MainExtensionTest.php index 91d3255f..75abf8e1 100644 --- a/tests/Twig/MainExtensionTest.php +++ b/tests/Twig/MainExtensionTest.php @@ -4,6 +4,7 @@ namespace Doctrine\Website\Tests\Twig; +use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Website\Assets\AssetIntegrityGenerator; use Doctrine\Website\Model\ProjectVersion; use Doctrine\Website\Tests\TestCase; @@ -51,12 +52,12 @@ public function testGetSearchBoxPlaceholder(): void $project = $this->createProject([ 'shortName' => 'ORM', - 'versions' => [ - [ + 'versions' => new ArrayCollection([ + new ProjectVersion([ 'slug' => 'latest', 'name' => '1.0', - ], - ], + ]), + ]), ]); $projectVersion = new ProjectVersion(['name' => '1.0']); diff --git a/tests/WebsiteBuilderTest.php b/tests/WebsiteBuilderTest.php index c912e055..9e314f3b 100644 --- a/tests/WebsiteBuilderTest.php +++ b/tests/WebsiteBuilderTest.php @@ -29,8 +29,6 @@ class WebsiteBuilderTest extends TestCase private string $rootDir; - private string $cacheDir; - private string $webpackBuildDir; private WebsiteBuilder&MockObject $websiteBuilder; @@ -43,7 +41,6 @@ protected function setUp(): void $this->sourceFileRepository = $this->createMock(SourceFileRepository::class); $this->sourceFilesBuilder = $this->createMock(SourceFilesBuilder::class); $this->rootDir = '/data/doctrine-website-build-staging'; - $this->cacheDir = '/data/doctrine-website-build-staging/cache'; $this->webpackBuildDir = '/data/doctrine-website-build-staging/.webpack-build'; $this->websiteBuilder = $this->getMockBuilder(WebsiteBuilder::class) @@ -54,7 +51,6 @@ protected function setUp(): void $this->sourceFileRepository, $this->sourceFilesBuilder, $this->rootDir, - $this->cacheDir, $this->webpackBuildDir, ]) ->onlyMethods(['filePutContents']) @@ -92,6 +88,5 @@ public function testBuild(): void $this->websiteBuilder->build($output, $buildDir, $env); self::assertSame($buildDir . '/frontend', $mirrored[$this->webpackBuildDir]); - self::assertSame($buildDir . '/website-data', $mirrored[$this->cacheDir . '/data']); } }