Skip to content

Commit

Permalink
Merge pull request #3338 from mhsdesign/feature/introduceFlowPackageK…
Browse files Browse the repository at this point in the history
…eyValueObject

TASK: Introduce internal flow package key value object
  • Loading branch information
mhsdesign committed Apr 1, 2024
2 parents 57b193e + 270c8bd commit 80fd123
Show file tree
Hide file tree
Showing 9 changed files with 367 additions and 150 deletions.
26 changes: 7 additions & 19 deletions Neos.Flow/Classes/Composer/ComposerUtility.php
Expand Up @@ -12,13 +12,14 @@
*/

use Neos\Flow\Package\FlowPackageInterface;
use Neos\Flow\Package\FlowPackageKey;
use Neos\Utility\ObjectAccess;
use Neos\Utility\Files;

/**
* Utility to access composer information like composer manifests (composer.json) and the lock file.
*
* Meant to be used only inside the Flow package management code.
* @internal Only meant to be used only inside the Flow package management code.
*/
class ComposerUtility
{
Expand Down Expand Up @@ -125,28 +126,15 @@ public static function isFlowPackageType(string $packageType): bool
return false;
}

/**
* Determines the composer package name ("vendor/foo-bar") from the Flow package key ("Vendor.Foo.Bar")
*
* @param string $packageKey
* @return string
*/
public static function getComposerPackageNameFromPackageKey(string $packageKey): string
{
$nameParts = explode('.', $packageKey);
$vendor = array_shift($nameParts);
return strtolower($vendor . '/' . implode('-', $nameParts));
}

/**
* Write a composer manifest for the package.
*
* @param string $manifestPath
* @param string $packageKey
* @param FlowPackageKey $packageKey
* @param array $composerManifestData
* @return array the manifest data written
*/
public static function writeComposerManifest(string $manifestPath, string $packageKey, array $composerManifestData = []): array
public static function writeComposerManifest(string $manifestPath, FlowPackageKey $packageKey, array $composerManifestData = []): array
{
$manifest = [
'description' => ''
Expand All @@ -156,19 +144,19 @@ public static function writeComposerManifest(string $manifestPath, string $packa
$manifest = array_merge($manifest, $composerManifestData);
}
if (!isset($manifest['name']) || empty($manifest['name'])) {
$manifest['name'] = static::getComposerPackageNameFromPackageKey($packageKey);
$manifest['name'] = $packageKey->deriveComposerPackageName();
}

if (!isset($manifest['require']) || empty($manifest['require'])) {
$manifest['require'] = ['neos/flow' => '*'];
}

if (!isset($manifest['autoload'])) {
$namespace = str_replace('.', '\\', $packageKey) . '\\';
$namespace = str_replace('.', '\\', $packageKey->value) . '\\';
$manifest['autoload'] = ['psr-4' => [$namespace => FlowPackageInterface::DIRECTORY_CLASSES]];
}

$manifest['extra']['neos']['package-key'] = $packageKey;
$manifest['extra']['neos']['package-key'] = $packageKey->value;

if (defined('JSON_PRETTY_PRINT')) {
file_put_contents(Files::concatenatePaths([$manifestPath, 'composer.json']), json_encode($manifest, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
Expand Down
130 changes: 130 additions & 0 deletions Neos.Flow/Classes/Package/FlowPackageKey.php
@@ -0,0 +1,130 @@
<?php

declare(strict_types=1);

namespace Neos\Flow\Package;

use Neos\Flow\Annotations as Flow;
use Neos\Flow\Composer\ComposerUtility;

/**
* (Legacy) Flow representation of a package key.
*
* With the rise of composer each package _has_ a key like "vendor/foo-bar".
* But before the adaption Flow already established the package keys like "Vendor.Foo.Bar",
* which is represented and validated by this value object.
*
* The Flow package keys are currently inferred from the composer manifest {@see FlowPackageKey::deriveFromManifestOrPath()},
* and can also be tried to be reverse calculated: {@see FlowPackageKey::deriveComposerPackageName()}
*
* The idea around the Flow package key is obsolete since composer and will eventually be replaced.
* Still major parts of Flow depend on the concept.
*
* @internal Only meant to be used only inside the Flow package management code. Should NOT leak into public APIs.
*/
final readonly class FlowPackageKey
{
public const PATTERN = '/^[a-z0-9]+\.(?:[a-z0-9][\.a-z0-9]*)+$/i';

/**
* @Flow\Autowiring(false)
*/
private function __construct(
public string $value
) {
if (!self::isPackageKeyValid($value)) {
throw new Exception\InvalidPackageKeyException('The package key "' . $value . '" is invalid', 1220722210);
}
}

public static function fromString(string $value)
{
return new self($value);
}

/**
* Check the conformance of the given package key
*
* @param string $string The package key to validate
* @return boolean If the package key is valid, returns true otherwise false
* @internal please use {@see PackageManager::isPackageKeyValid()}
*/
public static function isPackageKeyValid(string $string): bool
{
return preg_match(self::PATTERN, $string) === 1;
}

/**
* Resolves package key from Composer manifest
*
* If it is a Flow package the name of the containing directory will be used.
*
* Else if the composer name of the package matches the first part of the lowercased namespace of the package, the mixed
* case version of the composer name / namespace will be used, with backslashes replaced by dots.
*
* Else the composer name will be used with the slash replaced by a dot
*/
public static function deriveFromManifestOrPath(array $manifest, string $packagePath): self
{
$definedFlowPackageKey = $manifest['extra']['neos']['package-key'] ?? null;

if ($definedFlowPackageKey && self::isPackageKeyValid($definedFlowPackageKey)) {
return new self($definedFlowPackageKey);
}

$composerName = $manifest['name'];
$autoloadNamespace = null;
$type = $manifest['type'] ?? null;
if (isset($manifest['autoload']['psr-0']) && is_array($manifest['autoload']['psr-0'])) {
$namespaces = array_keys($manifest['autoload']['psr-0']);
$autoloadNamespace = reset($namespaces);
}
return self::derivePackageKeyInternal($composerName, $type, $packagePath, $autoloadNamespace);
}

/**
* Derive a flow package key from the given information.
* The order of importance is:
*
* - package install path
* - first found autoload namespace
* - composer name
*/
private static function derivePackageKeyInternal(string $composerName, ?string $packageType, string $packagePath, ?string $autoloadNamespace): self
{
$packageKey = '';

if ($packageType !== null && ComposerUtility::isFlowPackageType($packageType)) {
$lastSegmentOfPackagePath = substr(trim($packagePath, '/'), strrpos(trim($packagePath, '/'), '/') + 1);
if (str_contains($lastSegmentOfPackagePath, '.')) {
$packageKey = $lastSegmentOfPackagePath;
}
}

if ($autoloadNamespace !== null && (self::isPackageKeyValid($packageKey) === false)) {
$packageKey = str_replace('\\', '.', $autoloadNamespace);
}

if (self::isPackageKeyValid($packageKey) === false) {
$packageKey = str_replace('/', '.', $composerName);
}

$packageKey = trim($packageKey, '.');
$packageKey = preg_replace('/[^A-Za-z0-9.]/', '', $packageKey);

return new self($packageKey);
}

/**
* Determines the composer package name ("vendor/foo-bar") from the Flow package key ("Vendor.Foo.Bar")
*
* WARNING: This is NOT necessary the reverse calculation when the package key was inferred via {@see self::deriveFromManifestOrPath()}
* For example vendor/foo-bar will become vendor.foobar which in turn will be converted via this method to vendor/foobar
*/
public function deriveComposerPackageName(): string
{
$nameParts = explode('.', $this->value);
$vendor = array_shift($nameParts);
return strtolower($vendor . '/' . implode('-', $nameParts));
}
}
38 changes: 19 additions & 19 deletions Neos.Flow/Classes/Package/PackageFactory.php
Expand Up @@ -25,49 +25,49 @@ class PackageFactory
*
* @param string $packagesBasePath the base install path of packages,
* @param string $packagePath path to package, relative to base path
* @param string $packageKey key / name of the package
* @param FlowPackageKey $packageKey key / name of the package
* @param string $composerName
* @param array $autoloadConfiguration Autoload configuration as defined in composer.json
* @param array $packageClassInformation
* @return PackageInterface|PackageKeyAwareInterface
* @param array{className: class-string<PackageInterface>, pathAndFilename: string}|null $packageClassInformation
* @return PackageInterface&PackageKeyAwareInterface
* @throws Exception\CorruptPackageException
*/
public function create($packagesBasePath, $packagePath, $packageKey, $composerName, array $autoloadConfiguration = [], array $packageClassInformation = null)
public function create(string $packagesBasePath, string $packagePath, FlowPackageKey $packageKey, string $composerName, array $autoloadConfiguration = [], array $packageClassInformation = null): PackageInterface
{
$absolutePackagePath = Files::concatenatePaths([$packagesBasePath, $packagePath]) . '/';

if ($packageClassInformation === null) {
$packageClassInformation = $this->detectFlowPackageFilePath($packageKey, $absolutePackagePath);
}

$packageClassName = Package::class;
if (!empty($packageClassInformation)) {
$packageClassName = $packageClassInformation['className'];
$packageClassPath = !empty($packageClassInformation['pathAndFilename']) ? Files::concatenatePaths([$absolutePackagePath, $packageClassInformation['pathAndFilename']]) : null;
}
$packageClassName = $packageClassInformation['className'];

if (!empty($packageClassPath)) {
if (!empty($packageClassInformation['pathAndFilename'])) {
$packageClassPath = Files::concatenatePaths([$absolutePackagePath, $packageClassInformation['pathAndFilename']]);
require_once($packageClassPath);
}

$package = new $packageClassName($packageKey, $composerName, $absolutePackagePath, $autoloadConfiguration);
/** dynamic construction {@see GenericPackage::__construct} */
$package = new $packageClassName($packageKey->value, $composerName, $absolutePackagePath, $autoloadConfiguration);
if (!$package instanceof PackageInterface) {
throw new Exception\CorruptPackageException(sprintf('The package class of package "%s" does not implement \Neos\Flow\Package\PackageInterface. Check the file "%s".', $packageKey, $packageClassInformation['pathAndFilename']), 1427193370);
throw new Exception\CorruptPackageException(sprintf('The package class of package "%s" does not implement \Neos\Flow\Package\PackageInterface. Check the file "%s".', $packageKey->value, $packageClassInformation['pathAndFilename']), 1427193370);
}
if (!$package instanceof PackageKeyAwareInterface) {
throw new Exception\CorruptPackageException(sprintf('The package class of package "%s" does not implement \Neos\Flow\Package\PackageKeyAwareInterface. Check the file "%s".', $packageKey->value, $packageClassInformation['pathAndFilename']), 1711665156);
}

return $package;
}

/**
* Detects if the package contains a package file and returns the path and classname.
*
* @param string $packageKey The package key
* @param FlowPackageKey $packageKey The package key
* @param string $absolutePackagePath Absolute path to the package
* @return array The path to the package file and classname for this package or an empty array if none was found.
* @return array{className: class-string<PackageInterface>, pathAndFilename: string} The path to the package file and classname for this package or an empty array if none was found.
* @throws Exception\CorruptPackageException
* @throws Exception\InvalidPackagePathException
*/
public function detectFlowPackageFilePath($packageKey, $absolutePackagePath)
public function detectFlowPackageFilePath(FlowPackageKey $packageKey, $absolutePackagePath): array
{
if (!is_dir($absolutePackagePath)) {
throw new Exception\InvalidPackagePathException(sprintf('The given package path "%s" is not a readable directory.', $absolutePackagePath), 1445904440);
Expand All @@ -80,7 +80,7 @@ public function detectFlowPackageFilePath($packageKey, $absolutePackagePath)

$possiblePackageClassPaths = [
Files::concatenatePaths(['Classes', 'Package.php']),
Files::concatenatePaths(['Classes', str_replace('.', '/', $packageKey), 'Package.php'])
Files::concatenatePaths(['Classes', str_replace('.', '/', $packageKey->value), 'Package.php'])
];

$foundPackageClassPaths = array_filter($possiblePackageClassPaths, function ($packageClassPathAndFilename) use ($absolutePackagePath) {
Expand All @@ -93,7 +93,7 @@ public function detectFlowPackageFilePath($packageKey, $absolutePackagePath)
}

if (count($foundPackageClassPaths) > 1) {
throw new Exception\CorruptPackageException(sprintf('The package "%s" contains multiple possible "Package.php" files. Please make sure that only one "Package.php" exists in the autoload root(s) of your Flow package.', $packageKey), 1457454840);
throw new Exception\CorruptPackageException(sprintf('The package "%s" contains multiple possible "Package.php" files. Please make sure that only one "Package.php" exists in the autoload root(s) of your Flow package.', $packageKey->value), 1457454840);
}

$packageClassPathAndFilename = reset($foundPackageClassPaths);
Expand All @@ -102,7 +102,7 @@ public function detectFlowPackageFilePath($packageKey, $absolutePackagePath)
$packageClassContents = file_get_contents($absolutePackageClassPath);
$packageClassName = (new PhpAnalyzer($packageClassContents))->extractFullyQualifiedClassName();
if ($packageClassName === null) {
throw new Exception\CorruptPackageException(sprintf('The package "%s" does not contain a valid package class. Check if the file "%s" really contains a class.', $packageKey, $packageClassPathAndFilename), 1327587091);
throw new Exception\CorruptPackageException(sprintf('The package "%s" does not contain a valid package class. Check if the file "%s" really contains a class.', $packageKey->value, $packageClassPathAndFilename), 1327587091);
}

return ['className' => $packageClassName, 'pathAndFilename' => $packageClassPathAndFilename];
Expand Down
3 changes: 2 additions & 1 deletion Neos.Flow/Classes/Package/PackageInterface.php
Expand Up @@ -18,7 +18,8 @@
*/
interface PackageInterface
{
const PATTERN_MATCH_PACKAGEKEY = '/^[a-z0-9]+\.(?:[a-z0-9][\.a-z0-9]*)+$/i';
/** @deprecated with Flow 9, please use {@see PackageManager::isPackageKeyValid()} instead. */
const PATTERN_MATCH_PACKAGEKEY = FlowPackageKey::PATTERN;
const DEFAULT_COMPOSER_TYPE = 'neos-package';

/**
Expand Down

0 comments on commit 80fd123

Please sign in to comment.