├── .gitignore ├── README.md ├── composer.json └── src └── Composer ├── InstallerScript ├── EntryPoint.php └── RootDirectory.php ├── InstallerScripts.php └── Typo3EntryPointFinder.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /public 3 | /composer.lock 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # helhum/typo3-composer-setup 2 | 3 | This is a composer package that uses typo3/cms-composer-installers 4 | to generate entry points for the web directory instead of symlinking them. 5 | 6 | This means no symlinks to files are required any more, not even the symlinked 7 | autoload file inside the typo3/cms package 8 | 9 | In case `typo3/cms` is required instead of the individual TYPO3 core packages, 10 | this package will also set up symlinks to all required core extensions. 11 | No other core extensions are exposed and detected by TYPO3 except the ones 12 | that are required by composer installed packages (including the root package). 13 | 14 | This package requires `typo3/cms-composer-installers` `^1.4`, which requires PHP > 7.0 15 | 16 | In case you need the legacy `cli_dispatch.phpsh` entry point in TYPO3v7/v8 you can 17 | install the [pagemachine/typo3-composer-legacy-cli](https://packagist.org/packages/pagemachine/typo3-composer-legacy-cli) package. 18 | 19 | ## Installation 20 | 21 | `composer require helhum/typo3-composer-setup` 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "helhum/typo3-composer-setup", 3 | "description": "Set up required entry points for TYPO3 web directory", 4 | "type": "library", 5 | "license": "GPL-2.0-or-later", 6 | "authors": [ 7 | { 8 | "name": "Helmut Hummel", 9 | "email": "info@helhum.io", 10 | "homepage": "https://helhum.io", 11 | "role": "Developer" 12 | } 13 | ], 14 | "require": { 15 | "typo3/cms-composer-installers": "^1.4 || ^2.0 || ^3.0", 16 | "typo3/minimal": "^7 || ^8 || ^9 || ^10" 17 | }, 18 | "suggest": { 19 | "helhum/typo3-console": "TYPO3 Console is highly recommended for any TYPO3 composer setup.", 20 | "pagemachine/typo3-composer-legacy-cli": "Provides the legacy cli_dispatch.phpsh entry point for TYPO3." 21 | }, 22 | "require-dev": { 23 | "typo3/cms-core": "^8.7.10 || ^9.5.2 || ^10.2.0", 24 | "typo3-console/php-server-command": "^0.1.0" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "Helhum\\Typo3ComposerSetup\\": "src" 29 | } 30 | }, 31 | "extra": { 32 | "branch-alias": { 33 | "dev-master": "1.x-dev" 34 | }, 35 | "typo3/cms": { 36 | "web-dir": "public" 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Composer/InstallerScript/EntryPoint.php: -------------------------------------------------------------------------------- 1 | source = $source; 46 | $this->target = $target; 47 | $this->additionalCode = $additionalCode; 48 | } 49 | 50 | public function run(Event $event): bool 51 | { 52 | $composer = $event->getComposer(); 53 | $filesystem = new Filesystem(); 54 | 55 | $entryPointContent = file_get_contents($this->source); 56 | $autoloadFile = $composer->getConfig()->get('vendor-dir') . '/autoload.php'; 57 | 58 | $entryPointContent = preg_replace( 59 | '/(\$classLoader = require )[^;]*;$/m', 60 | '\1' . $filesystem->findShortestPathCode($this->target, $autoloadFile) . ';' 61 | . chr(10) 62 | . $this->additionalCode, 63 | $entryPointContent 64 | ); 65 | 66 | $filesystem->ensureDirectoryExists(dirname($this->target)); 67 | file_put_contents($this->target, $entryPointContent); 68 | 69 | return true; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Composer/InstallerScript/RootDirectory.php: -------------------------------------------------------------------------------- 1 | rootDir = $rootDir; 77 | $this->publishStrategy = $publishStrategy; 78 | } 79 | 80 | /** 81 | * Prepare the web directory with symlinks 82 | * 83 | * @param Event $event 84 | * @return bool 85 | */ 86 | public function run(Event $event): bool 87 | { 88 | $this->io = $event->getIO(); 89 | $this->composer = $event->getComposer(); 90 | $this->filesystem = new Filesystem(); 91 | $this->isDevMode = $event->isDevMode(); 92 | $backendDir = $this->rootDir . self::$typo3Dir; 93 | 94 | $this->io->writeError('Setting up TYPO3 Core Extension directories'); 95 | 96 | $localRepository = $this->composer->getRepositoryManager()->getLocalRepository(); 97 | $typo3Package = $localRepository->findPackage('typo3/cms', '*'); 98 | $sourcesDir = $this->composer->getInstallationManager()->getInstallPath($typo3Package); 99 | 100 | $source = $sourcesDir . self::$typo3Dir . self::$systemExtensionsDir; 101 | $target = $backendDir . self::$systemExtensionsDir; 102 | 103 | $this->ensureOldLinksRemoved($target); 104 | $this->filesystem->ensureDirectoryExists($target); 105 | 106 | $coreExtKeys = $this->getCoreExtensionKeysFromTypo3Package($typo3Package); 107 | $fileSystem = new \Symfony\Component\Filesystem\Filesystem(); 108 | $installedSystemExtensions = glob($target . '/*'); 109 | foreach ($installedSystemExtensions as $installedSystemExtension) { 110 | if (!in_array(basename($installedSystemExtension), $coreExtKeys, true)) { 111 | if ($this->filesystem->isJunction($installedSystemExtension)) { 112 | $this->filesystem->removeJunction($installedSystemExtension); 113 | } else { 114 | $fileSystem->remove($installedSystemExtension); 115 | } 116 | } 117 | } 118 | 119 | foreach ($coreExtKeys as $coreExtKey) { 120 | $extensionSource = $source . '/' . $coreExtKey; 121 | $extensionTarget = $target . '/' . $coreExtKey; 122 | if ($this->publishStrategy === self::PUBLISH_STRATEGY_LINK) { 123 | if (file_exists($extensionTarget)) { 124 | continue; 125 | } 126 | if (Platform::isWindows()) { 127 | // Implement symlinks as NTFS junctions on Windows 128 | $this->filesystem->junction($extensionSource, $extensionTarget); 129 | } else { 130 | $shortestPath = $this->filesystem->findShortestPath($extensionTarget, $extensionSource); 131 | $extensionTarget = rtrim($extensionTarget, '/'); 132 | $fileSystem->symlink($shortestPath, $extensionTarget); 133 | } 134 | } elseif ($this->publishStrategy === self::PUBLISH_STRATEGY_MIRROR) { 135 | $fileSystem->mirror($extensionSource, $extensionTarget, null, ['delete' => true]); 136 | } else { 137 | throw new \UnexpectedValueException('Publish strategy can only be one of "mirror" or "link"'); 138 | } 139 | } 140 | 141 | return true; 142 | } 143 | 144 | private function ensureOldLinksRemoved(string $systemDir) 145 | { 146 | $typo3Dir = dirname($systemDir); 147 | $indexPhp = dirname($typo3Dir) . '/index.php'; 148 | foreach ([$systemDir, $typo3Dir, $indexPhp] as $possibleLink) { 149 | if (is_link($possibleLink)) { 150 | unlink($possibleLink); 151 | } 152 | } 153 | } 154 | 155 | /** 156 | * @param PackageInterface $typo3Package 157 | * @return array 158 | */ 159 | private function getCoreExtensionKeysFromTypo3Package(PackageInterface $typo3Package): array 160 | { 161 | $coreExtensionKeys = []; 162 | $frameworkPackages = []; 163 | foreach ($typo3Package->getReplaces() as $name => $_) { 164 | if (is_string($name) && strpos($name, 'typo3/cms-') === 0) { 165 | $frameworkPackages[] = $name; 166 | } 167 | } 168 | $installedPackages = $this->composer->getRepositoryManager()->getLocalRepository()->getCanonicalPackages(); 169 | $rootPackage = $this->composer->getPackage(); 170 | $installedPackages[$rootPackage->getName()] = $rootPackage; 171 | foreach ($installedPackages as $package) { 172 | $requires = $package->getRequires(); 173 | if ($package === $rootPackage && $this->isDevMode) { 174 | $requires = array_merge($requires, $package->getDevRequires()); 175 | } 176 | foreach ($requires as $name => $_) { 177 | if (is_string($name) && in_array($name, $frameworkPackages, true)) { 178 | $extensionKey = $this->determineExtKeyFromPackageName($name); 179 | $this->io->writeError(sprintf('The package "%s" requires: "%s"', $package->getName(), $name), true, IOInterface::DEBUG); 180 | $this->io->writeError(sprintf('The extension key for package "%s" is: "%s"', $name, $extensionKey), true, IOInterface::DEBUG); 181 | $coreExtensionKeys[$name] = $extensionKey; 182 | } 183 | } 184 | } 185 | return $coreExtensionKeys; 186 | } 187 | 188 | /** 189 | * @param string $packageName 190 | * @return string 191 | */ 192 | private function determineExtKeyFromPackageName(string $packageName): string 193 | { 194 | return str_replace(['typo3/cms-', '-'], ['', '_'], $packageName); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/Composer/InstallerScripts.php: -------------------------------------------------------------------------------- 1 | getComposer(); 37 | $pluginConfig = Config::load($composer); 38 | $webDir = $pluginConfig->get('web-dir'); 39 | 40 | $entryPointFinder = new Typo3EntryPointFinder( 41 | $composer->getRepositoryManager()->getLocalRepository(), 42 | $composer->getInstallationManager() 43 | ); 44 | 45 | foreach ($entryPointFinder->find($webDir) as $entryPoint) { 46 | $scriptDispatcher->addInstallerScript( 47 | new EntryPoint( 48 | $entryPoint['source'], 49 | $entryPoint['target'] 50 | ), 51 | 80 52 | ); 53 | } 54 | 55 | $rootDir = $pluginConfig->get('root-dir'); 56 | $typo3CmsPackage = $event->getComposer()->getRepositoryManager()->getLocalRepository()->findPackage('typo3/cms', '*'); 57 | if ( 58 | $typo3CmsPackage 59 | && !class_exists(\Helhum\Typo3NoSymlinkInstall\Composer\InstallerScripts::class) 60 | && !class_exists(\Helhum\Typo3SecureWeb\Composer\InstallerScripts::class) 61 | ) { 62 | $scriptDispatcher->addInstallerScript( 63 | new RootDirectory($rootDir, RootDirectory::PUBLISH_STRATEGY_LINK), 64 | 90 65 | ); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Composer/Typo3EntryPointFinder.php: -------------------------------------------------------------------------------- 1 | 9 | * All rights reserved 10 | * 11 | * The GNU General Public License can be found at 12 | * http://www.gnu.org/copyleft/gpl.html. 13 | * A copy is found in the text file GPL.txt and important notices to the license 14 | * from the author is found in LICENSE.txt distributed with these scripts. 15 | * 16 | * 17 | * This script is distributed in the hope that it will be useful, 18 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | * GNU General Public License for more details. 21 | * 22 | * This copyright notice MUST APPEAR in all copies of the script! 23 | ***************************************************************/ 24 | 25 | use Composer\Installer\InstallationManager; 26 | use Composer\Repository\WritableRepositoryInterface; 27 | 28 | class Typo3EntryPointFinder 29 | { 30 | private static $defaultEntryPoints = [ 31 | 'frontend' => [ 32 | 'target' => 'index.php', 33 | ], 34 | 'backend' => [ 35 | 'target' => 'typo3/index.php', 36 | ], 37 | 'install' => [ 38 | 'target' => 'typo3/install.php', 39 | ], 40 | ]; 41 | 42 | /** 43 | * @var WritableRepositoryInterface 44 | */ 45 | private $repository; 46 | 47 | /** 48 | * @var InstallationManager 49 | */ 50 | private $installationManager; 51 | 52 | public function __construct(WritableRepositoryInterface $repository, InstallationManager $installationManager) 53 | { 54 | $this->repository = $repository; 55 | $this->installationManager = $installationManager; 56 | } 57 | 58 | public function find(string $targetPath): array 59 | { 60 | $entryPoints = self::$defaultEntryPoints; 61 | $typo3CmsPackage = $this->repository->findPackage('typo3/cms', '*'); 62 | if ($typo3CmsPackage) { 63 | $cmsInstallPath = $this->installationManager->getInstallPath($typo3CmsPackage); 64 | $frontendPackagePath = $cmsInstallPath . '/typo3/sysext/frontend'; 65 | $backendPackagePath = $cmsInstallPath . '/typo3/sysext/backend'; 66 | $installPackagePath = $cmsInstallPath . '/typo3/sysext/install'; 67 | } else { 68 | $frontendPackage = $this->repository->findPackage('typo3/cms-frontend', '*'); 69 | $frontendPackagePath = $this->installationManager->getInstallPath($frontendPackage); 70 | $backendPackage = $this->repository->findPackage('typo3/cms-backend', '*'); 71 | $backendPackagePath = $this->installationManager->getInstallPath($backendPackage); 72 | $installPackage = $this->repository->findPackage('typo3/cms-install', '*'); 73 | $installPackagePath = $this->installationManager->getInstallPath($installPackage); 74 | } 75 | 76 | if (file_exists($frontendSourceFile = $frontendPackagePath . '/Resources/Private/Php/frontend.php')) { 77 | $entryPoints['frontend']['source'] = $frontendSourceFile; 78 | } elseif (isset($cmsInstallPath)) { 79 | $entryPoints['frontend']['source'] = $cmsInstallPath . '/index.php'; 80 | } else { 81 | throw new \UnexpectedValueException('Could not determine frontend entry point. typo3/cms is not installed?', 1502706253); 82 | } 83 | 84 | if (file_exists($backendSourceFile = $backendPackagePath . '/Resources/Private/Php/backend.php')) { 85 | $entryPoints['backend']['source'] = $backendSourceFile; 86 | } elseif (isset($cmsInstallPath)) { 87 | $entryPoints['backend']['source'] = $cmsInstallPath . '/typo3/index.php'; 88 | } else { 89 | throw new \UnexpectedValueException('Could not determine backend entry point. typo3/cms is not installed?', 1502706254); 90 | } 91 | 92 | if (file_exists($installSourceFile = $installPackagePath . '/Resources/Private/Php/install.php')) { 93 | $entryPoints['install']['source'] = $installSourceFile; 94 | } elseif (isset($cmsInstallPath)) { 95 | $entryPoints['install']['source'] = $cmsInstallPath . '/typo3/sysext/install/Start/Install.php'; 96 | $entryPoints['install']['target'] = 'typo3/sysext/install/Start/Install.php'; 97 | } else { 98 | throw new \UnexpectedValueException('Could not determine install tool entry point. typo3/cms is not installed?', 1502706255); 99 | } 100 | 101 | $entryPoints['frontend']['target'] = $targetPath . '/' . $entryPoints['frontend']['target']; 102 | $entryPoints['backend']['target'] = $targetPath . '/' . $entryPoints['backend']['target']; 103 | $entryPoints['install']['target'] = $targetPath . '/' . $entryPoints['install']['target']; 104 | 105 | return $entryPoints; 106 | } 107 | } 108 | --------------------------------------------------------------------------------