44
55use CodedMonkey \Dirigent \Doctrine \Repository \PackageRepository ;
66use CodedMonkey \Dirigent \Validator \UniquePackage ;
7+ use Composer \Package \Version \VersionParser ;
78use Composer \Pcre \Preg ;
89use Doctrine \Common \Collections \ArrayCollection ;
910use Doctrine \Common \Collections \Collection ;
@@ -88,6 +89,8 @@ class Package extends TrackedEntity
8889 */
8990 private array $ cachedVersions ;
9091
92+ private array $ sortedVersions ;
93+
9194 public function __construct ()
9295 {
9396 $ this ->installations = new PackageInstallations ($ this );
@@ -339,19 +342,55 @@ public function setDumpedAt(?\DateTimeImmutable $dumpedAt): void
339342 $ this ->dumpedAt = $ dumpedAt ;
340343 }
341344
345+ public function getBrowsableRepositoryUrl (): ?string
346+ {
347+ if (!$ this ->repositoryUrl ) {
348+ return null ;
349+ }
350+
351+ if (!Preg::isMatch ('{^https?://}i ' , $ this ->repositoryUrl )) {
352+ return null ;
353+ }
354+
355+ return $ this ->repositoryUrl ;
356+ }
357+
358+ public function getPrettyBrowsableRepositoryUrl (): ?string
359+ {
360+ if (null === $ url = $ this ->getBrowsableRepositoryUrl ()) {
361+ return null ;
362+ }
363+
364+ $ url = preg_replace ('#^https?://# ' , '' , $ url );
365+
366+ return $ url ;
367+ }
368+
369+ /**
370+ * @return Version[]
371+ */
372+ public function getSortedVersions (): array
373+ {
374+ if (!isset ($ this ->sortedVersions )) {
375+ $ this ->sortedVersions = $ this ->versions ->toArray ();
376+
377+ usort ($ this ->sortedVersions , [static ::class, 'sortVersions ' ]);
378+ }
379+
380+ return $ this ->sortedVersions ;
381+ }
382+
342383 /**
343- * Returns the default branch or latest version of the package.
384+ * Returns the default branch or the latest version of the package.
344385 */
345386 public function getDefaultVersion (): ?Version
346387 {
347- $ versions = $ this ->versions -> toArray ();
388+ $ versions = $ this ->getSortedVersions ();
348389
349390 if (!count ($ versions )) {
350391 return null ;
351392 }
352393
353- usort ($ versions , [static ::class, 'sortVersions ' ]);
354-
355394 $ latestVersion = reset ($ versions );
356395 foreach ($ versions as $ version ) {
357396 if ($ version ->isDefaultBranch ()) {
@@ -363,18 +402,17 @@ public function getDefaultVersion(): ?Version
363402 }
364403
365404 /**
366- * Returns the latest (numbered) version of the package, or the default version if no versions were found.
405+ * The latest (numbered) version of the package, or the default version if no versions were found.
367406 */
368407 public function getLatestVersion (): ?Version
369408 {
370- $ versions = $ this ->versions -> toArray ();
409+ $ versions = $ this ->getSortedVersions ();
371410
372411 if (!count ($ versions )) {
373412 return null ;
374413 }
375414
376- usort ($ versions , [static ::class, 'sortVersions ' ]);
377-
415+ // Return the first non-development version
378416 foreach ($ versions as $ version ) {
379417 if (!$ version ->isDevelopment ()) {
380418 return $ version ;
@@ -384,6 +422,132 @@ public function getLatestVersion(): ?Version
384422 return $ this ->getDefaultVersion ();
385423 }
386424
425+ /**
426+ * The latest version of each major version.
427+ *
428+ * @return Version[]
429+ */
430+ public function getActiveVersions (): array
431+ {
432+ $ activeVersions = [];
433+ $ activePrereleaseVersions = [];
434+
435+ foreach ($ this ->getSortedVersions () as $ version ) {
436+ if ('stable ' !== VersionParser::parseStability ($ version ->getNormalizedVersion ())) {
437+ continue ;
438+ }
439+
440+ [$ majorVersion , $ minorVersion ] = explode ('. ' , $ version ->getNormalizedVersion ());
441+
442+ if ('0 ' === $ majorVersion ) {
443+ $ prereleaseVersion = "$ majorVersion. $ minorVersion " ;
444+
445+ $ activePrereleaseVersions [$ prereleaseVersion ] ??= $ version ;
446+ if (version_compare ($ version ->getNormalizedVersion (), $ activePrereleaseVersions [$ prereleaseVersion ]->getNormalizedVersion (), '> ' )) {
447+ $ activePrereleaseVersions [$ prereleaseVersion ] = $ version ;
448+ }
449+
450+ continue ;
451+ }
452+
453+ $ activeVersions [$ majorVersion ] ??= $ version ;
454+ if (version_compare ($ version ->getNormalizedVersion (), $ activeVersions [$ majorVersion ]->getNormalizedVersion (), '> ' )) {
455+ $ activeVersions [$ majorVersion ] = $ version ;
456+ }
457+ }
458+
459+ $ activeDevelopmentVersions = [];
460+ $ activePrereleaseDevelopmentVersions = [];
461+
462+ // Find newer unstable releases of active versions
463+ foreach ($ this ->getSortedVersions () as $ version ) {
464+ if (in_array (VersionParser::parseStability ($ version ->getNormalizedVersion ()), ['stable ' , 'dev ' ], true )) {
465+ continue ;
466+ }
467+
468+ [$ majorVersion , $ minorVersion ] = explode ('. ' , $ version ->getNormalizedVersion ());
469+
470+ $ developmentVersion = "$ majorVersion. $ minorVersion " ;
471+
472+ if ('0 ' === $ majorVersion ) {
473+ if (isset ($ activePrereleaseVersions [$ developmentVersion ]) && !version_compare ($ version ->getNormalizedVersion (), $ activePrereleaseVersions [$ developmentVersion ]->getNormalizedVersion (), '> ' )) {
474+ continue ;
475+ }
476+
477+ $ activePrereleaseDevelopmentVersions [$ developmentVersion ] ??= $ version ;
478+ if (version_compare ($ version ->getNormalizedVersion (), $ activePrereleaseDevelopmentVersions [$ developmentVersion ]->getNormalizedVersion (), '> ' )) {
479+ $ activePrereleaseDevelopmentVersions [$ developmentVersion ] = $ version ;
480+ }
481+
482+ continue ;
483+ }
484+
485+ if (isset ($ activeVersions [$ majorVersion ]) && !version_compare ($ version ->getNormalizedVersion (), $ activeVersions [$ majorVersion ]->getNormalizedVersion (), '> ' )) {
486+ continue ;
487+ }
488+
489+ $ activeDevelopmentVersions [$ developmentVersion ] ??= $ version ;
490+ if (version_compare ($ version ->getNormalizedVersion (), $ activeDevelopmentVersions [$ developmentVersion ]->getNormalizedVersion (), '> ' )) {
491+ $ activeDevelopmentVersions [$ version ->getNormalizedVersion ()] = $ version ;
492+ }
493+ }
494+
495+ $ activeVersions = [...$ activeVersions , ...$ activeDevelopmentVersions ];
496+
497+ if (count ($ activeVersions )) {
498+ usort ($ activeVersions , [static ::class, 'sortVersions ' ]);
499+
500+ return $ activeVersions ;
501+ }
502+
503+ // Only show pre-release versions (0.x.x) if no versions after 1.0.0 was found
504+ $ activePrereleaseVersions = [...$ activePrereleaseVersions , ...$ activePrereleaseDevelopmentVersions ];
505+
506+ usort ($ activePrereleaseVersions , [static ::class, 'sortVersions ' ]);
507+
508+ return $ activePrereleaseVersions ;
509+ }
510+
511+ /**
512+ * All non-development versions that are not part of the active versions.
513+ *
514+ * @return Version[]
515+ */
516+ public function getHistoricalVersions (): array
517+ {
518+ $ historicalVersions = array_filter ($ this ->getSortedVersions (), static fn (Version $ version ) => !$ version ->isDevelopment ());
519+
520+ return array_diff ($ historicalVersions , $ this ->getActiveVersions ());
521+ }
522+
523+ /**
524+ * All development versions associated with a version number (2.0.x-dev, 0.1.x-dev).
525+ *
526+ * @return Version[]
527+ */
528+ public function getDevVersions (): array
529+ {
530+ return array_filter ($ this ->getSortedVersions (), static function (Version $ version ) {
531+ if (str_ends_with ($ version ->getNormalizedVersion (), '.9999999-dev ' )) {
532+ return true ;
533+ }
534+
535+ static $ parser = new VersionParser ();
536+
537+ return $ version ->hasVersionAlias () && str_ends_with ($ parser ->normalize ($ version ->getVersionAlias ()), '.9999999-dev ' );
538+ });
539+ }
540+
541+ /**
542+ * All development versions associated with a branch (dev-main, dev-master, dev-develop).
543+ *
544+ * @return Version[]
545+ */
546+ public function getDevBranchVersions (): array
547+ {
548+ return array_filter ($ this ->getSortedVersions (), static fn (Version $ version ) => str_starts_with ($ version ->getNormalizedVersion (), 'dev- ' ));
549+ }
550+
387551 public static function sortVersions (Version $ a , Version $ b ): int
388552 {
389553 $ aVersion = $ a ->getNormalizedVersion ();
0 commit comments