Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
/files/
/modules/
/themes/
/composer-addons/modules/*
!/composer-addons/modules/.gitkeep
/composer-addons/themes/*
!/composer-addons/themes/.gitkeep
/application/test/config/database.ini
/application/test/.phpunit.result.cache
/application/asset/css/font-awesome/
Expand Down
1 change: 1 addition & 0 deletions .php-cs-fixer.dist.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
->exclude('application/data/doctrine-proxies')
->exclude('application/data/media-types')
->exclude('application/data/overrides')
->exclude('composer-addons')
->exclude('config')
->exclude('files')
->exclude('modules')
Expand Down
1 change: 1 addition & 0 deletions application/config/application.config.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
'module_paths' => [
'Omeka' => OMEKA_PATH . '/application',
OMEKA_PATH . '/modules',
OMEKA_PATH . '/composer-addons/modules',
],
'config_glob_paths' => [
OMEKA_PATH . '/config/local.config.php',
Expand Down
8 changes: 6 additions & 2 deletions application/data/composer-addon-installer/composer.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
{
"name": "omeka/composer-addon-installer",
"version": "2.0",
"version": "3.0",
"description": "Composer plugin to install Omeka S modules and themes into composer-addons/modules/ and composer-addons/themes/.",
"type": "composer-plugin",
"license": "GPL-3.0",
"require": {
"php": ">=8.1",
"composer-plugin-api": "^2.0"
},
"autoload": {
"psr-4": {"Omeka\\Composer\\": "src/"}
"psr-4": {
"Omeka\\Composer\\": "src/"
}
},
"extra": {
"class": "Omeka\\Composer\\AddonInstallerPlugin"
Expand Down
104 changes: 91 additions & 13 deletions application/data/composer-addon-installer/src/AddonInstaller.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,46 +4,124 @@
use Composer\Package\PackageInterface;
use Composer\Installer\LibraryInstaller;

/**
* Composer installer for Omeka S modules and themes.
*
* Installs packages to composer-addons/modules/ or composer-addons/themes/ based on type.
* Name transformations align with composer/installers OmekaSInstaller.
*
* Supports extra options:
* - installer-name: Explicit install name (overrides auto-detection)
*/
class AddonInstaller extends LibraryInstaller
{
/**
* Gets the name this package is to be installed with, either from the
* <pre>extra.install-name</pre> property or the package name.
* Gets the name this package is to be installed with.
*
* Priority:
* 1. extra.installer-name (explicit)
* 2. Auto-transformation based on package type
*
* For modules: removes prefixes/suffixes and converts to CamelCase
* For themes: removes prefixes/suffixes, keeps lowercase with hyphens
*
* @return string
*/
public static function getInstallName(PackageInterface $package)
public static function getInstallName(PackageInterface $package): string
{
$extra = $package->getExtra();

// Support both installer-name (composer/installers) and install-name
// (legacy).
if (isset($extra['installer-name'])) {
return $extra['installer-name'];
}
if (isset($extra['install-name'])) {
return $extra['install-name'];
}

$packageName = $package->getPrettyName();
$slashPos = strpos($packageName, '/');
if ($slashPos === false) {
throw new \InvalidArgumentException('Addon package names must contain a slash');
throw new \InvalidArgumentException('Add-on package names must contain a slash'); // @translate
}

$addonName = substr($packageName, $slashPos + 1);
return $addonName;
$name = substr($packageName, $slashPos + 1);
$type = $package->getType();

if ($type === 'omeka-s-module') {
return static::inflectModuleName($name);
}

if ($type === 'omeka-s-theme') {
return static::inflectThemeName($name);
}

return $name;
}

/**
* Transform module name: remove prefixes/suffixes, convert to CamelCase.
*
* Examples:
* - omeka-s-module-common → Common
* - value-suggest → ValueSuggest
* - bulk-import-module → BulkImport
* - neatline-omeka-s → Neatline
* - module-lessonplans → Lessonplans
*
* @param string $name
* @return string
*/
protected static function inflectModuleName($name): string
{
// Remove Omeka prefixes/suffixes.
$name = preg_replace('/^(omeka-?s?-?)?(module-)?/', '', $name);
$name = preg_replace('/(-module)?(-omeka-?s?)?$/', '', $name);

// Convert kebab-case to CamelCase.
$name = strtr($name, ['-' => ' ']);
$name = strtr(ucwords($name), [' ' => '']);

return $name;
}

public function getInstallPath(PackageInterface $package)
/**
* Transform theme name: remove prefixes/suffixes, keep lowercase.
*
* Examples:
* - omeka-s-theme-repository → repository
* - my-custom-theme → my-custom
* - flavor-theme-omeka → flavor
*
* @param string $name
* @return string
*/
protected static function inflectThemeName($name): string
{
// Remove Omeka prefixes/suffixes.
$name = preg_replace('/^(omeka-?s?-?)?(theme-)?/', '', $name);
$name = preg_replace('/(-theme)?(-omeka-?s?)?$/', '', $name);

return $name;
}

public function getInstallPath(PackageInterface $package): string
{
$addonName = static::getInstallName($package);
switch ($package->getType()) {
case 'omeka-s-theme':
return 'themes/' . $addonName;
case 'omeka-s-module':
return 'modules/' . $addonName;
return 'composer-addons/modules/' . $addonName;
case 'omeka-s-theme':
return 'composer-addons/themes/' . $addonName;
default:
throw new \InvalidArgumentException('Invalid Omeka S addon package type');
throw new \InvalidArgumentException('Invalid Omeka S add-on package type'); // @translate
}
}

public function supports($packageType)
public function supports($packageType): bool
{
return in_array($packageType, ['omeka-s-theme', 'omeka-s-module']);
return $packageType === 'omeka-s-module'
|| $packageType === 'omeka-s-theme';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
use Composer\IO\IOInterface;
use Composer\Plugin\PluginInterface;

/**
* Composer plugin for Omeka S add-on installation.
*
* Registers the AddonInstaller for modules and themes.
*/
class AddonInstallerPlugin implements PluginInterface
{
public function activate(Composer $composer, IOInterface $io)
Expand Down
142 changes: 142 additions & 0 deletions application/data/scripts/install-addon-deps.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
#!/usr/bin/env php
<?php
/**
* Install composer dependencies for a git-cloned module or theme.
*
* For add-ons installed via git clone in modules/ or themes/, dependencies
* are not installed automatically. This script reads the add-on composer.json
* and installs its dependencies via the root Omeka composer.
*
* Note: Add-ons installed via `composer require` (in composer-addons/modules/ or
* composer-addons/themes/) have their dependencies installed automatically.
*
* Usage:
* php application/data/scripts/install-addon-deps.php ModuleName
* php application/data/scripts/install-addon-deps.php --theme theme-name
* php application/data/scripts/install-addon-deps.php --dry-run ModuleName
*
* Options:
* --theme Specify a theme instead of a module
* --dry-run Show what would be installed without actually installing
*/
$args = array_slice($argv, 1);
$dryRun = false;
$isTheme = false;
$addonName = null;

foreach ($args as $arg) {
if ($arg === '--dry-run') {
$dryRun = true;
} elseif ($arg === '--theme') {
$isTheme = true;
} elseif (strpos($arg, '--') !== 0) {
$addonName = $arg;
}
}

if (!$addonName) {
echo "Usage: php application/data/scripts/install-addon-deps.php [--dry-run] [--theme] Name\n";
echo "\nOptions:\n";
echo " --theme Specify a theme instead of a module\n";
echo " --dry-run Show what would be installed without actually installing\n";
exit(1);
}

// Find addon path
if ($isTheme) {
$possiblePaths = [
dirname(__DIR__, 3) . '/themes/' . $addonName,
dirname(__DIR__, 3) . '/composer-addons/themes/' . $addonName,
];
$addonType = 'Theme';
} else {
$possiblePaths = [
dirname(__DIR__, 3) . '/modules/' . $addonName,
dirname(__DIR__, 3) . '/composer-addons/modules/' . $addonName,
];
$addonType = 'Module';
}

$addonPath = null;
foreach ($possiblePaths as $path) {
if (is_dir($path)) {
$addonPath = $path;
break;
}
}

if (!$addonPath) {
echo "Error: $addonType '$addonName' not found.\n";
exit(1);
}

$composerJson = $addonPath . '/composer.json';
if (!file_exists($composerJson)) {
echo "Error: No composer.json found in $addonPath\n";
exit(1);
}

$json = json_decode(file_get_contents($composerJson), true);
if (!$json) {
echo "Error: Invalid composer.json\n";
exit(1);
}

$require = $json['require'] ?? [];

if (empty($require)) {
echo "No dependencies to install for $addonName.\n";
exit(0);
}

// Filter out packages provided by Omeka or PHP
$toInstall = [];
foreach ($require as $package => $version) {
// Skip Omeka core packages (provided by Omeka)
if (in_array($package, ['omeka/omeka-s', 'omeka/omeka-s-core'])) {
continue;
}
// Skip PHP extensions
if (strpos($package, 'ext-') === 0) {
continue;
}
// Skip PHP version constraint
if ($package === 'php') {
continue;
}
$toInstall[$package] = $version;
}

if (empty($toInstall)) {
echo "No external dependencies to install for $addonName.\n";
exit(0);
}

echo "Dependencies for $addonName:\n";
foreach ($toInstall as $package => $version) {
echo " - $package: $version\n";
}

if ($dryRun) {
echo "\n[Dry run] Would run:\n";
echo " composer require " . implode(' ', array_keys($toInstall)) . "\n";
exit(0);
}

echo "\nInstalling...\n";

$packages = implode(' ', array_map(function ($pkg, $ver) {
// Use version constraint if specific, otherwise let composer decide
if (preg_match('/^\^|~|>=|<=|>|<|\*/', $ver)) {
return escapeshellarg("$pkg:$ver");
}
return escapeshellarg($pkg);
}, array_keys($toInstall), $toInstall));

$command = "cd " . escapeshellarg(dirname(__DIR__, 3)) . " && composer require $packages --no-interaction 2>&1";

echo "Running: composer require " . implode(' ', array_keys($toInstall)) . "\n\n";

passthru($command, $exitCode);

exit($exitCode);
2 changes: 1 addition & 1 deletion application/src/Module/AbstractModule.php
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ public function getAutoloaderConfig()
return;
}

$autoloadPath = sprintf('%1$s/modules/%2$s/src', OMEKA_PATH, $namespace);
$autoloadPath = dirname($classInfo->getFileName()) . '/src';
return [
'Laminas\Loader\StandardAutoloader' => [
'namespaces' => [
Expand Down
Loading