Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Possibility to add second code source directory for modules local composer development using content root feature #1263

Closed
spalewski opened this issue Dec 15, 2022 · 5 comments · Fixed by #2504

Comments

@spalewski
Copy link

spalewski commented Dec 15, 2022

Is your feature request related to a problem? Please describe.

When I develop modules that will be distributed by composer, I use custom "extensions" directory than app/code. Like described here: https://www.yireo.com/blog/2019-05-10-local-composer-development-with-magento2 .

Describe the solution you'd like (*)

It would be nice, to have option to select custom module as a second code source for plugin, so all plugin features will be also available there.

Additional context

https://www.yireo.com/blog/2019-05-10-local-composer-development-with-magento2

From Slack discussion:

Image

About content roots:

https://www.jetbrains.com/help/idea/content-roots.html

@m2-assistant
Copy link

m2-assistant bot commented Dec 15, 2022

Hi @spalewski. Thank you for your report.
To speed up processing of this issue, make sure that you provided sufficient information.

Add a comment to assign the issue: @magento I am working on this


@VitaliyBoyko
Copy link
Contributor

Hi @spalewski

Have you tried to specify the Magento installation as included path?
Screenshot 2022-12-17 at 15 17 32
Screenshot 2022-12-17 at 15 14 59

@spalewski
Copy link
Author

spalewski commented Dec 19, 2022

Hi @VitaliyBoyko, yes i tried that but I does not fix this feature. My expected behavior is to have something like second app/code folder and have all plugin features there. Like create new module, plugin etc.. Right now:
image

@shaughnlgh
Copy link

I found a solution that might work for others and does not require the developers of this plugin to add a feature for this.

Our team actively works on custom Magento 2 modules within the vendor directory. In order to use this plugin, the module needs to be in the app/code directory. My workaround is simple:

  • create symlinks from a given vendor namespace into the app/code directory.
  • for each module in the given vendor namespace, scan the module root dir for composer.json and load the true module name from autoload.psr-4 where the key is the Vendor/Package and the value is the source (most cases will be blank)
  • generate the relative path of the module that will be symlinked
  • unlink the symlink if it exists
  • add the symlink
  • update a .gitignore in the app/code directory

I put this into a simple script in my main Magento root scripts/SymlinkModules.php:

<?php

namespace YourProjectVendor\ComposerScripts;

class SymlinkModules
{
    /**
     * Execute
     *
     * @param string $vendorNamespace
     * @throws \JsonException
     */
    public static function execute(
        string $vendorNamespace
    ): void {
        if (!$vendorNamespace) {
            echo "Vendor namespace is required\n";
            return;
        }

        $rootDir = \dirname(__DIR__);
        $vendorDir = $rootDir . '/vendor/' . $vendorNamespace;
        $appCodeDir = $rootDir . '/app/code/';
        $gitignoreFile = $appCodeDir . '.gitignore';

        if (!is_dir($vendorDir)) {
            echo "No modules found in $vendorDir\n";
            return;
        }

        $modules = array_filter(scandir($vendorDir, SCANDIR_SORT_NONE), static function ($dir) use ($vendorDir) {
            return $dir !== '.' && $dir !== '..' && is_dir($vendorDir . '/' . $dir);
        });

        foreach ($modules as $module) {
            $composerJson = file_get_contents($vendorDir . '/' . $module . '/composer.json');
            if ($composerJson === false) {
                continue;
            }

            $composerConfig = json_decode($composerJson, true, 512, JSON_THROW_ON_ERROR);

            if (!isset($composerConfig['autoload']['psr-4'])) {
                continue;
            }

            $psr4 = $composerConfig['autoload']['psr-4'];
            $moduleName = array_key_first($psr4);
            $moduleName = str_replace('\\', '/', trim($moduleName, '\\'));
            $destination = $appCodeDir . $moduleName;

            $autoloadPath = array_values($psr4)[0];
            $source = $vendorDir . '/' . $module . (empty($autoloadPath) ? '' : '/' . $autoloadPath);

            // Create parent directories if not exist
            $destinationDir = \dirname($destination);
            if (!is_dir($destinationDir)
                && !mkdir($destinationDir, 0755, true)
                && !is_dir($destinationDir)
            ) {
                throw new \RuntimeException(\sprintf('Directory "%s" was not created', $destinationDir));
            }

            // Remove any existing symlink or directory
            if (is_link($destination) || is_dir($destination)) {
                unlink($destination);
            }

            // Calculate relative path from the destination to the source
            $relativeSource = self::getRelativePath($destinationDir, $source);

            // Create the symlink using the relative path
            symlink($relativeSource, $destination);
            echo "Symlinked $relativeSource -> $destination\n";

            // Update the .gitignore with the module path
            self::updateGitignore($gitignoreFile, $moduleName);
        }
    }

    /**
     * Get relative path
     *
     * @param string $from
     * @param string $to
     *
     * @return string
     */
    private static function getRelativePath(
        string $from,
        string $to
    ): string {
        // Normalize
        $from = realpath($from);
        $to = realpath($to);

        if (!$from || !$to) {
            return '';
        }

        $fromParts = explode(DIRECTORY_SEPARATOR, $from);
        $toParts = explode(DIRECTORY_SEPARATOR, $to);

        // Remove common parts from the beginning of the path
        while (count($fromParts) && count($toParts) && $fromParts[0] === $toParts[0]) {
            array_shift($fromParts);
            array_shift($toParts);
        }

        // Go up from the source
        $relativePath = str_repeat('..' . DIRECTORY_SEPARATOR, count($fromParts));

        // Add the remaining target path
        $relativePath .= implode(DIRECTORY_SEPARATOR, $toParts);

        return $relativePath;
    }

    /**
     * Update gitignore file
     *
     * @param string $gitignoreFile
     * @param string $module
     *
     * @return void
     */
    private static function updateGitignore(
        string $gitignoreFile,
        string $module
    ): void {
        $modulePath = $module;

        // If .gitignore doesn't exist, create it
        if (!is_file($gitignoreFile)) {
            echo ".gitignore doesn't exist, creating it...\n";
            file_put_contents($gitignoreFile, "# Generated by SymlinkModules Script\n");
        }

        // Check if the path is already in .gitignore to avoid duplication
        $gitignoreContents = file_get_contents($gitignoreFile);

        // Split the contents by line and check for exact matches
        foreach (explode(PHP_EOL, $gitignoreContents) as $line) {
            if (trim($line) === $modulePath) {
                //echo "$modulePath already exists in .gitignore\n";
                return;
            }
        }

        // If not found, append to .gitignore
        file_put_contents($gitignoreFile, "\n$modulePath", FILE_APPEND);
        //echo "Added $modulePath to .gitignore\n";
    }
}

Then append the following to you projects composer.json:

  "autoload-dev": {
        "psr-4": {
            "YourProjectVendor\\ComposerScripts\\": "scripts/"
        }
    },
   "scripts": {
        "post-install-cmd": [
            "([ $COMPOSER_DEV_MODE -eq 0 ] || VENDOR_NAMESPACE=your_vendor_namespace php -r \"require_once 'vendor/autoload.php'; \\\\YourProjectVendor\\\\ComposerScripts\\\\SymlinkModules::execute(getenv('VENDOR_NAMESPACE'));\")"
        ],
        "post-update-cmd": [
            "([ $COMPOSER_DEV_MODE -eq 0 ] || VENDOR_NAMESPACE=your_vendor_namespace php -r \"require_once 'vendor/autoload.php'; \\\\YourProjectVendor\\\\ComposerScripts\\\\SymlinkModules::execute(getenv('VENDOR_NAMESPACE'));\")"
        ]
    }

Replace your_vendor_namespace with your custom namespace found in the vendor directory.

Then whenever you run composer install or composer update, the script will symlink the modules into the app/code directory and you will have all this plugins functionality available to you as if you where working in in app/code.

SIDE NOTE: If you are using warden development environment, this method will also work, however, mutagen is responsible for bi-directional syncing of files between your machine and the docker container, and by default symlinks are ignored. You need to update your projects mutagen config and set sync.defaults.symlink.mode: "posix-raw".

@shaughnlgh
Copy link

And if you dont have time for the above solution, there is an even quicker one:

  • Go to IntelliJ IDEA / PHPSTORM Settings -> Keymap
  • Search for Magento 2
  • Bind a key mapping to anyone the available options

Then in your vendor modules, target the root of the module (make sure the directory is highlighted), and hit those hot keys you just bound. You should see the relevant modal for the plugin show up and on submission, the relevant files / dirs will be generated at your current highlighted target.

NOTE: This method works for most abilities that this plugin provides, but it will not work certain features like plugin overrides, since the targets are only read out of app/code

@VitaliyBoyko VitaliyBoyko changed the title Possibility to add second code source directory for modules local composer development Possibility to add second code source directory for modules local composer development using content root feature Feb 26, 2025
@VitaliyBoyko VitaliyBoyko self-assigned this Mar 1, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment